@trazum/cli 1.37.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/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { join, resolve as resolvePath } from 'node:path';
4
4
  import { gunzipSync } from 'node:zlib';
5
- import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, detectFromSource, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
5
+ import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, buildPlan, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, detectFromSource, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
6
6
  import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
7
7
  import { dayOf, formatGap, median, spanDays } from './time.js';
8
8
  // Everything that reads the filesystem, on its own entry point so the web
@@ -26,6 +26,7 @@ const c = {
26
26
  const VALUE_FLAGS = new Set([
27
27
  'against',
28
28
  'from-log',
29
+ 'min-usd',
29
30
  // `route` takes a path here, and the flag is deliberately not `--prompt`:
30
31
  // everywhere else in this tool `--prompt` names a marked prompt *inside* a
31
32
  // source file, and reusing it for a path would be a trap laid for the reader.
@@ -159,6 +160,12 @@ function levelFlag(args, config, t) {
159
160
  * model id. It beats the default because reading the code is better than
160
161
  * assuming, and loses to config because being told is better than reading.
161
162
  */
163
+ /**
164
+ * The file names a usage log answers to, shared by every command that reads a
165
+ * directory of them. One list, because two commands disagreeing on what counts
166
+ * as a log would be the same directory billing differently by verb.
167
+ */
168
+ const LOG_EXTENSIONS = ['.jsonl', '.ndjson', '.log', '.json'];
162
169
  /**
163
170
  * One usage log, gzip included, shared by every command that reads one.
164
171
  *
@@ -279,6 +286,8 @@ const COMMAND_FLAGS = {
279
286
  check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
280
287
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
281
288
  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'],
289
+ plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
290
+ verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
282
291
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
283
292
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
284
293
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -1636,6 +1645,243 @@ function isoDate() {
1636
1645
  * metered API calls somebody was actually billed for — the bill exists wherever
1637
1646
  * Trazum happens to be running, so the host has no bearing on it.
1638
1647
  */
1648
+ /**
1649
+ * `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
1650
+ *
1651
+ * The plan predicted; this holds the prediction to the log that came after
1652
+ * it. Three outcomes and never two — arrived, did not arrive, cannot be told
1653
+ * — because "cannot be told" rendered as "arrived" is how every other tool
1654
+ * congratulates a team for a workload that merely vanished. With `--gate`,
1655
+ * a broken promise is a failing exit code: a different and more useful gate
1656
+ * than "spend went up".
1657
+ */
1658
+ async function commandVerify(args, pricing, t) {
1659
+ const planPath = args.positional[0];
1660
+ if (planPath === undefined)
1661
+ throw new Error(t.verify.noTarget());
1662
+ const againstPath = stringFlag(args, 'against');
1663
+ if (againstPath === undefined)
1664
+ throw new Error(t.verify.needsAgainst());
1665
+ let plan;
1666
+ try {
1667
+ const parsed = JSON.parse(await readFile(planPath, 'utf8'));
1668
+ if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.actions)) {
1669
+ throw new Error(t.verify.badPlan(planPath));
1670
+ }
1671
+ plan = parsed;
1672
+ }
1673
+ catch (error) {
1674
+ if (error instanceof SyntaxError)
1675
+ throw new Error(t.verify.badPlan(planPath));
1676
+ throw error;
1677
+ }
1678
+ const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
1679
+ const READABLE = [...LOG_EXTENSIONS, ...GZ];
1680
+ const target = await stat(againstPath).catch(() => null);
1681
+ let files = [againstPath];
1682
+ if (target?.isDirectory()) {
1683
+ const entries = await readdir(againstPath, { withFileTypes: true });
1684
+ files = entries
1685
+ .filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
1686
+ .map((entry) => join(againstPath, entry.name))
1687
+ .sort((a, b) => a.localeCompare(b));
1688
+ if (files.length === 0)
1689
+ throw new Error(t.profile.noLogsInDirectory(againstPath, READABLE.join(', ')));
1690
+ }
1691
+ const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
1692
+ const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
1693
+ const report = profileUsage(raw, { catalogue: pricing });
1694
+ const verification = verifyPlan(plan, report, { currentPricingLastReviewed: pricing.lastReviewed });
1695
+ const gate = boolFlag(args, 'gate');
1696
+ const n = (value) => value.toLocaleString(t.numberLocale);
1697
+ const lines = (md) => {
1698
+ const out = [];
1699
+ const actionLine = (v) => {
1700
+ const name = v.action.label === UNLABELLED ? t.profile.unlabelled() : v.action.label;
1701
+ const rows = [];
1702
+ rows.push(t.verify.action(v.action.kind, name, v.action.model, v.outcome));
1703
+ if (v.outcome === 'cannot-tell' && v.reason !== null)
1704
+ rows.push(t.verify.reason(v.reason));
1705
+ if (v.action.kind === 'route' || v.action.kind === 'route+batch') {
1706
+ if (v.outcome !== 'cannot-tell') {
1707
+ rows.push(t.verify.routeObserved(String(v.observed.dearestModel ?? ''), formatUsd(Number(v.observed.onTargetUsd ?? 0)), formatUsd(Number(v.observed.onOldModelUsd ?? 0))));
1708
+ }
1709
+ if (v.action.kind === 'route+batch' && v.outcome !== 'cannot-tell')
1710
+ rows.push(t.verify.batchUnobservable());
1711
+ }
1712
+ if (v.action.kind === 'fix-truncation' && v.outcome === 'not-arrived') {
1713
+ rows.push(t.verify.truncationObserved(formatUsd(Number(v.observed.retryBillUsd ?? 0))));
1714
+ }
1715
+ if (v.action.kind === 'fix-caching' && v.outcome !== 'cannot-tell') {
1716
+ rows.push(t.verify.cacheObserved(formatUsd(Number(v.observed.deltaUsd ?? 0)), v.outcome));
1717
+ }
1718
+ if (v.attribution?.calls !== undefined) {
1719
+ rows.push(t.verify.attribution(n(Math.round(v.attribution.calls.before)), n(Math.round(v.attribution.calls.after)), n(Math.round(v.attribution.outputPerCallTokens?.before ?? 0)), n(Math.round(v.attribution.outputPerCallTokens?.after ?? 0))));
1720
+ }
1721
+ return rows;
1722
+ };
1723
+ const heading = t.verify.heading(n(verification.actions.length), verification.planCreatedAt === null ? null : verification.planCreatedAt.slice(0, 10));
1724
+ out.push(md ? `## ${heading}` : heading);
1725
+ out.push(t.verify.counts(n(verification.arrived), n(verification.notArrived), n(verification.cannotTell)));
1726
+ if (verification.pricesChanged) {
1727
+ out.push(t.verify.pricesChanged(verification.planPricing, verification.currentPricing));
1728
+ }
1729
+ for (const v of verification.actions) {
1730
+ out.push('');
1731
+ const [head, ...rest] = actionLine(v);
1732
+ out.push(md ? `### ${head}` : `→ ${head}`);
1733
+ for (const row of rest)
1734
+ out.push(md ? `- ${row}` : ` · ${row}`);
1735
+ }
1736
+ out.push('');
1737
+ out.push(t.verify.footer());
1738
+ return out;
1739
+ };
1740
+ await writeMarkdown(args, () => lines(true).join('\n'));
1741
+ if (boolFlag(args, 'json')) {
1742
+ console.log(JSON.stringify(verification, null, 2));
1743
+ }
1744
+ else {
1745
+ const [head, ...rest] = lines(false);
1746
+ console.log(c.bold(head));
1747
+ for (const row of rest) {
1748
+ console.log(row === '' ? '' : ` ${wrap(row, 74, ' ')}`);
1749
+ }
1750
+ }
1751
+ if (gate) {
1752
+ if (verification.gateFailures > 0) {
1753
+ console.error(c.red(t.verify.gateFailed(n(verification.gateFailures), n(verification.actions.length))));
1754
+ process.exitCode = 1;
1755
+ }
1756
+ else {
1757
+ console.log(c.green(t.verify.gateOk()));
1758
+ }
1759
+ }
1760
+ }
1761
+ /**
1762
+ * `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
1763
+ *
1764
+ * The composition (route and batch on one slice never summed) happens in
1765
+ * core's `buildPlan`; this command owns the I/O and the rendering. The plan
1766
+ * saves as a dated JSON file on request, which is what makes verifying it
1767
+ * against a later log possible at all — a prediction nobody wrote down is a
1768
+ * prediction nobody can be held to.
1769
+ */
1770
+ async function commandPlan(args, pricing, t) {
1771
+ const path = args.positional[0];
1772
+ if (path === undefined)
1773
+ throw new Error(t.plan.noTarget());
1774
+ const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
1775
+ const READABLE = [...LOG_EXTENSIONS, ...GZ];
1776
+ const target = await stat(path).catch(() => null);
1777
+ let files = [path];
1778
+ if (target?.isDirectory()) {
1779
+ const entries = await readdir(path, { withFileTypes: true });
1780
+ files = entries
1781
+ .filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
1782
+ .map((entry) => join(path, entry.name))
1783
+ .sort((a, b) => a.localeCompare(b));
1784
+ if (files.length === 0)
1785
+ throw new Error(t.profile.noLogsInDirectory(path, READABLE.join(', ')));
1786
+ }
1787
+ const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
1788
+ const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
1789
+ const report = profileUsage(raw, { catalogue: pricing });
1790
+ if (report.total.calls === 0)
1791
+ throw new Error(t.plan.nothingPriced());
1792
+ const levers = billLevers(report, { catalogue: pricing });
1793
+ const plan = buildPlan(report, levers, pricing.lastReviewed);
1794
+ const minUsd = typeof args.flags.get('min-usd') === 'string' ? numberFlag(args, 'min-usd', 0, t) : 0;
1795
+ const actions = plan.actions.filter((a) => (a.savingUsd ?? a.stakeUsd ?? 0) >= minUsd);
1796
+ const filtered = plan.actions.length - actions.length;
1797
+ const droppedUsd = plan.actions
1798
+ .filter((a) => (a.savingUsd ?? a.stakeUsd ?? 0) < minUsd)
1799
+ .reduce((sum, a) => sum + (a.savingUsd ?? a.stakeUsd ?? 0), 0);
1800
+ const n = (value) => value.toLocaleString(t.numberLocale);
1801
+ /**
1802
+ * The document's totals cover the actions the document holds — a filtered
1803
+ * plan whose totals still counted the filtered actions would be a file
1804
+ * that contradicts itself, and 1.39's verify would hold it to money it
1805
+ * cannot see. What --min-usd dropped is stated with its worth, never
1806
+ * silently.
1807
+ */
1808
+ const stamped = {
1809
+ ...plan,
1810
+ actions,
1811
+ projectedSavingUsd: actions.reduce((sum, a) => sum + (a.savingUsd ?? 0), 0),
1812
+ measuredStakeUsd: actions.reduce((sum, a) => sum + (a.stakeUsd ?? 0), 0),
1813
+ createdAt: new Date().toISOString(),
1814
+ };
1815
+ const outPath = stringFlag(args, 'out');
1816
+ if (outPath !== undefined) {
1817
+ await writeFile(outPath, `${JSON.stringify(stamped, null, 2)}\n`);
1818
+ }
1819
+ await writeMarkdown(args, () => {
1820
+ const lines = [];
1821
+ lines.push(`## ${t.plan.heading(n(actions.length), formatUsd(plan.totalUsd))}`);
1822
+ lines.push('');
1823
+ lines.push(t.plan.totals(formatUsd(stamped.projectedSavingUsd), formatUsd(stamped.measuredStakeUsd)));
1824
+ if (plan.span === null) {
1825
+ lines.push('');
1826
+ lines.push(`_${t.plan.noClock()}_`);
1827
+ }
1828
+ for (const action of actions) {
1829
+ const name = action.label === UNLABELLED ? t.profile.unlabelled() : action.label;
1830
+ const money = action.savingUsd !== null
1831
+ ? t.plan.projected(formatUsd(action.savingUsd))
1832
+ : t.plan.staked(formatUsd(action.stakeUsd ?? 0));
1833
+ lines.push('');
1834
+ lines.push(`### ${t.plan.action(action.kind, name, action.model)} — ${money}`);
1835
+ if (action.detail.routeTo !== undefined)
1836
+ lines.push(`- ${t.plan.routeTo(action.detail.routeTo.displayName)}`);
1837
+ for (const assumption of action.assumes)
1838
+ lines.push(`- ${t.plan.assume(assumption)}`);
1839
+ if (action.check !== null)
1840
+ lines.push(`- ${t.plan.check(action.check)}`);
1841
+ }
1842
+ if (filtered > 0) {
1843
+ lines.push('');
1844
+ lines.push(`_${t.plan.filtered(n(filtered), formatUsd(minUsd), formatUsd(droppedUsd))}_`);
1845
+ }
1846
+ lines.push('');
1847
+ lines.push(`_${t.plan.footer()}_`);
1848
+ return lines.join('\n');
1849
+ });
1850
+ if (boolFlag(args, 'json')) {
1851
+ console.log(JSON.stringify(stamped, null, 2));
1852
+ return;
1853
+ }
1854
+ console.log(c.bold(t.plan.heading(n(actions.length), formatUsd(plan.totalUsd))));
1855
+ console.log(` ${wrap(t.plan.totals(formatUsd(stamped.projectedSavingUsd), formatUsd(stamped.measuredStakeUsd)), 74, ' ')}`);
1856
+ if (plan.span === null) {
1857
+ console.log(` ${c.dim(wrap(t.plan.noClock(), 74, ' '))}`);
1858
+ }
1859
+ for (const action of actions) {
1860
+ const name = action.label === UNLABELLED ? t.profile.unlabelled() : action.label;
1861
+ const money = action.savingUsd !== null
1862
+ ? t.plan.projected(formatUsd(action.savingUsd))
1863
+ : t.plan.staked(formatUsd(action.stakeUsd ?? 0));
1864
+ console.log();
1865
+ console.log(` ${c.green('→')} ${c.bold(t.plan.action(action.kind, name, action.model))} ${money}`);
1866
+ if (action.detail.routeTo !== undefined) {
1867
+ console.log(` ${c.dim(t.plan.routeTo(action.detail.routeTo.displayName))}`);
1868
+ }
1869
+ for (const assumption of action.assumes) {
1870
+ console.log(` ${c.yellow('?')} ${c.dim(wrap(t.plan.assume(assumption), 72, ' '))}`);
1871
+ }
1872
+ if (action.check !== null) {
1873
+ console.log(` ${c.dim(wrap(t.plan.check(action.check), 72, ' '))}`);
1874
+ }
1875
+ }
1876
+ if (filtered > 0) {
1877
+ console.log();
1878
+ console.log(` ${c.dim(wrap(t.plan.filtered(n(filtered), formatUsd(minUsd), formatUsd(droppedUsd)), 74, ' '))}`);
1879
+ }
1880
+ console.log();
1881
+ console.log(` ${c.dim(wrap(t.plan.footer(), 74, ' '))}`);
1882
+ if (outPath !== undefined)
1883
+ console.log(c.dim(wrap(t.plan.wrote(outPath), 74, '')));
1884
+ }
1639
1885
  async function commandProfile(args, config, pricing, t) {
1640
1886
  const path = args.positional[0];
1641
1887
  if (path === undefined) {
@@ -1658,7 +1904,6 @@ async function commandProfile(args, config, pricing, t) {
1658
1904
  * directory holding nothing readable is an error naming what it looked for,
1659
1905
  * not an empty report.
1660
1906
  */
1661
- const LOG_EXTENSIONS = ['.jsonl', '.ndjson', '.log', '.json'];
1662
1907
  /**
1663
1908
  * The same names, gzipped — which is what a rotated log actually looks like
1664
1909
  * a day after it rotates.
@@ -4801,6 +5046,12 @@ async function main() {
4801
5046
  case 'profile':
4802
5047
  await commandProfile(args, config, pricing, t);
4803
5048
  break;
5049
+ case 'plan':
5050
+ await commandPlan(args, pricing, t);
5051
+ break;
5052
+ case 'verify':
5053
+ await commandVerify(args, pricing, t);
5054
+ break;
4804
5055
  case 'route':
4805
5056
  await commandRoute(args, pricing, t);
4806
5057
  break;