@trazum/cli 1.38.0 → 1.40.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 +2 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +101 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +53 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +227 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +123 -0
- package/src/i18n/es.ts +126 -0
- package/src/i18n/types.ts +55 -1
- package/src/index.ts +269 -0
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, buildPlan, 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, buildHistory, buildPlan, storedReportFrom, 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
|
|
@@ -287,6 +287,8 @@ const COMMAND_FLAGS = {
|
|
|
287
287
|
baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
|
|
288
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
289
|
plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
|
|
290
|
+
verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
|
|
291
|
+
history: ['json', 'markdown-out'],
|
|
290
292
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
291
293
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
292
294
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -1644,6 +1646,224 @@ function isoDate() {
|
|
|
1644
1646
|
* metered API calls somebody was actually billed for — the bill exists wherever
|
|
1645
1647
|
* Trazum happens to be running, so the host has no bearing on it.
|
|
1646
1648
|
*/
|
|
1649
|
+
/**
|
|
1650
|
+
* `trazum history <dir>` — many reports over many periods, as one series.
|
|
1651
|
+
*
|
|
1652
|
+
* Derived from *stored* `--json` documents, never re-parsed logs: a team can
|
|
1653
|
+
* keep a year of reports and throw the raw logs away, which is what the
|
|
1654
|
+
* privacy story requires anyway. Shapes are named — a climb, a decay, the
|
|
1655
|
+
* same action planned twice — and no series, however long, becomes a
|
|
1656
|
+
* forecast.
|
|
1657
|
+
*/
|
|
1658
|
+
async function commandHistory(args, t) {
|
|
1659
|
+
const path = args.positional[0];
|
|
1660
|
+
if (path === undefined)
|
|
1661
|
+
throw new Error(t.history.noTarget());
|
|
1662
|
+
const target = await stat(path).catch(() => null);
|
|
1663
|
+
if (!target?.isDirectory())
|
|
1664
|
+
throw new Error(t.history.noTarget());
|
|
1665
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1666
|
+
const files = entries
|
|
1667
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
1668
|
+
.map((entry) => join(path, entry.name))
|
|
1669
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1670
|
+
const reports = [];
|
|
1671
|
+
const plans = [];
|
|
1672
|
+
const unrecognized = [];
|
|
1673
|
+
for (const file of files) {
|
|
1674
|
+
let parsed;
|
|
1675
|
+
try {
|
|
1676
|
+
parsed = JSON.parse(await readFile(file, 'utf8'));
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
unrecognized.push(file);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
const report = storedReportFrom(file, parsed);
|
|
1683
|
+
if (report !== null) {
|
|
1684
|
+
reports.push(report);
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const maybePlan = parsed;
|
|
1688
|
+
if (maybePlan?.schemaVersion === 1 && Array.isArray(maybePlan.actions)) {
|
|
1689
|
+
plans.push(maybePlan);
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
unrecognized.push(file);
|
|
1693
|
+
}
|
|
1694
|
+
const history = buildHistory(reports, plans);
|
|
1695
|
+
if (history.periods.length < 3) {
|
|
1696
|
+
throw new Error(t.history.needsThree(String(history.periods.length)));
|
|
1697
|
+
}
|
|
1698
|
+
const stamped = { ...history, unrecognizedFiles: unrecognized };
|
|
1699
|
+
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1700
|
+
const day = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
1701
|
+
const pct = (value) => `${(value * 100).toFixed(1)}%`;
|
|
1702
|
+
const runLine = (run) => {
|
|
1703
|
+
if (run.kind === 'label-spend-climbing') {
|
|
1704
|
+
const name = run.subject === UNLABELLED ? t.profile.unlabelled() : run.subject;
|
|
1705
|
+
return t.history.runLabel(name, n(run.periods), run.sinceName, formatUsd(run.from), formatUsd(run.to));
|
|
1706
|
+
}
|
|
1707
|
+
if (run.kind === 'model-share-climbing') {
|
|
1708
|
+
return t.history.runModel(run.subject, n(run.periods), run.sinceName, pct(run.from), pct(run.to));
|
|
1709
|
+
}
|
|
1710
|
+
return t.history.runCache(n(run.periods), run.sinceName, pct(run.from), pct(run.to));
|
|
1711
|
+
};
|
|
1712
|
+
const lines = (md) => {
|
|
1713
|
+
const out = [];
|
|
1714
|
+
const first = history.periods[0];
|
|
1715
|
+
const last = history.periods[history.periods.length - 1];
|
|
1716
|
+
const heading = t.history.heading(n(history.periods.length), day(first.fromMs), day(last.toMs));
|
|
1717
|
+
out.push(md ? `## ${heading}` : heading);
|
|
1718
|
+
for (const period of history.periods) {
|
|
1719
|
+
const row = t.history.periodRow(period.name, formatUsd(period.totalUsd), n(period.calls), ((period.toMs - period.fromMs) / 86_400_000).toFixed(1));
|
|
1720
|
+
out.push(md ? `- ${row}` : ` ${row}`);
|
|
1721
|
+
}
|
|
1722
|
+
if (history.runs.length > 0)
|
|
1723
|
+
out.push('');
|
|
1724
|
+
for (const run of history.runs) {
|
|
1725
|
+
out.push(md ? `- ${runLine(run)}` : ` ! ${runLine(run)}`);
|
|
1726
|
+
}
|
|
1727
|
+
if (history.repeatedPlanActions.length > 0)
|
|
1728
|
+
out.push('');
|
|
1729
|
+
for (const repeat of history.repeatedPlanActions) {
|
|
1730
|
+
const name = repeat.label === UNLABELLED ? t.profile.unlabelled() : repeat.label;
|
|
1731
|
+
const row = t.history.repeated(repeat.kind, name, repeat.model, n(repeat.appearances), repeat.firstPlanned?.slice(0, 10) ?? null, repeat.lastPlanned?.slice(0, 10) ?? null);
|
|
1732
|
+
out.push(md ? `- ${row}` : ` ! ${row}`);
|
|
1733
|
+
}
|
|
1734
|
+
for (const name of history.undatedReports) {
|
|
1735
|
+
out.push(md ? `- ${t.history.undated(name)}` : ` ${t.history.undated(name)}`);
|
|
1736
|
+
}
|
|
1737
|
+
for (const name of unrecognized) {
|
|
1738
|
+
out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
|
|
1739
|
+
}
|
|
1740
|
+
out.push('');
|
|
1741
|
+
out.push(md ? `_${t.history.footer()}_` : ` ${t.history.footer()}`);
|
|
1742
|
+
return out;
|
|
1743
|
+
};
|
|
1744
|
+
await writeMarkdown(args, () => lines(true).join('\n'));
|
|
1745
|
+
if (boolFlag(args, 'json')) {
|
|
1746
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
const [head, ...rest] = lines(false);
|
|
1750
|
+
console.log(c.bold(head));
|
|
1751
|
+
for (const row of rest)
|
|
1752
|
+
console.log(row === '' ? '' : wrap(row, 76, ' '));
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
|
|
1756
|
+
*
|
|
1757
|
+
* The plan predicted; this holds the prediction to the log that came after
|
|
1758
|
+
* it. Three outcomes and never two — arrived, did not arrive, cannot be told
|
|
1759
|
+
* — because "cannot be told" rendered as "arrived" is how every other tool
|
|
1760
|
+
* congratulates a team for a workload that merely vanished. With `--gate`,
|
|
1761
|
+
* a broken promise is a failing exit code: a different and more useful gate
|
|
1762
|
+
* than "spend went up".
|
|
1763
|
+
*/
|
|
1764
|
+
async function commandVerify(args, pricing, t) {
|
|
1765
|
+
const planPath = args.positional[0];
|
|
1766
|
+
if (planPath === undefined)
|
|
1767
|
+
throw new Error(t.verify.noTarget());
|
|
1768
|
+
const againstPath = stringFlag(args, 'against');
|
|
1769
|
+
if (againstPath === undefined)
|
|
1770
|
+
throw new Error(t.verify.needsAgainst());
|
|
1771
|
+
let plan;
|
|
1772
|
+
try {
|
|
1773
|
+
const parsed = JSON.parse(await readFile(planPath, 'utf8'));
|
|
1774
|
+
if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.actions)) {
|
|
1775
|
+
throw new Error(t.verify.badPlan(planPath));
|
|
1776
|
+
}
|
|
1777
|
+
plan = parsed;
|
|
1778
|
+
}
|
|
1779
|
+
catch (error) {
|
|
1780
|
+
if (error instanceof SyntaxError)
|
|
1781
|
+
throw new Error(t.verify.badPlan(planPath));
|
|
1782
|
+
throw error;
|
|
1783
|
+
}
|
|
1784
|
+
const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
|
|
1785
|
+
const READABLE = [...LOG_EXTENSIONS, ...GZ];
|
|
1786
|
+
const target = await stat(againstPath).catch(() => null);
|
|
1787
|
+
let files = [againstPath];
|
|
1788
|
+
if (target?.isDirectory()) {
|
|
1789
|
+
const entries = await readdir(againstPath, { withFileTypes: true });
|
|
1790
|
+
files = entries
|
|
1791
|
+
.filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
|
|
1792
|
+
.map((entry) => join(againstPath, entry.name))
|
|
1793
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1794
|
+
if (files.length === 0)
|
|
1795
|
+
throw new Error(t.profile.noLogsInDirectory(againstPath, READABLE.join(', ')));
|
|
1796
|
+
}
|
|
1797
|
+
const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
|
|
1798
|
+
const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
|
|
1799
|
+
const report = profileUsage(raw, { catalogue: pricing });
|
|
1800
|
+
const verification = verifyPlan(plan, report, { currentPricingLastReviewed: pricing.lastReviewed });
|
|
1801
|
+
const gate = boolFlag(args, 'gate');
|
|
1802
|
+
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1803
|
+
const lines = (md) => {
|
|
1804
|
+
const out = [];
|
|
1805
|
+
const actionLine = (v) => {
|
|
1806
|
+
const name = v.action.label === UNLABELLED ? t.profile.unlabelled() : v.action.label;
|
|
1807
|
+
const rows = [];
|
|
1808
|
+
rows.push(t.verify.action(v.action.kind, name, v.action.model, v.outcome));
|
|
1809
|
+
if (v.outcome === 'cannot-tell' && v.reason !== null)
|
|
1810
|
+
rows.push(t.verify.reason(v.reason));
|
|
1811
|
+
if (v.action.kind === 'route' || v.action.kind === 'route+batch') {
|
|
1812
|
+
if (v.outcome !== 'cannot-tell') {
|
|
1813
|
+
rows.push(t.verify.routeObserved(String(v.observed.dearestModel ?? ''), formatUsd(Number(v.observed.onTargetUsd ?? 0)), formatUsd(Number(v.observed.onOldModelUsd ?? 0))));
|
|
1814
|
+
}
|
|
1815
|
+
if (v.action.kind === 'route+batch' && v.outcome !== 'cannot-tell')
|
|
1816
|
+
rows.push(t.verify.batchUnobservable());
|
|
1817
|
+
}
|
|
1818
|
+
if (v.action.kind === 'fix-truncation' && v.outcome === 'not-arrived') {
|
|
1819
|
+
rows.push(t.verify.truncationObserved(formatUsd(Number(v.observed.retryBillUsd ?? 0))));
|
|
1820
|
+
}
|
|
1821
|
+
if (v.action.kind === 'fix-caching' && v.outcome !== 'cannot-tell') {
|
|
1822
|
+
rows.push(t.verify.cacheObserved(formatUsd(Number(v.observed.deltaUsd ?? 0)), v.outcome));
|
|
1823
|
+
}
|
|
1824
|
+
if (v.attribution?.calls !== undefined) {
|
|
1825
|
+
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))));
|
|
1826
|
+
}
|
|
1827
|
+
return rows;
|
|
1828
|
+
};
|
|
1829
|
+
const heading = t.verify.heading(n(verification.actions.length), verification.planCreatedAt === null ? null : verification.planCreatedAt.slice(0, 10));
|
|
1830
|
+
out.push(md ? `## ${heading}` : heading);
|
|
1831
|
+
out.push(t.verify.counts(n(verification.arrived), n(verification.notArrived), n(verification.cannotTell)));
|
|
1832
|
+
if (verification.pricesChanged) {
|
|
1833
|
+
out.push(t.verify.pricesChanged(verification.planPricing, verification.currentPricing));
|
|
1834
|
+
}
|
|
1835
|
+
for (const v of verification.actions) {
|
|
1836
|
+
out.push('');
|
|
1837
|
+
const [head, ...rest] = actionLine(v);
|
|
1838
|
+
out.push(md ? `### ${head}` : `→ ${head}`);
|
|
1839
|
+
for (const row of rest)
|
|
1840
|
+
out.push(md ? `- ${row}` : ` · ${row}`);
|
|
1841
|
+
}
|
|
1842
|
+
out.push('');
|
|
1843
|
+
out.push(t.verify.footer());
|
|
1844
|
+
return out;
|
|
1845
|
+
};
|
|
1846
|
+
await writeMarkdown(args, () => lines(true).join('\n'));
|
|
1847
|
+
if (boolFlag(args, 'json')) {
|
|
1848
|
+
console.log(JSON.stringify(verification, null, 2));
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
const [head, ...rest] = lines(false);
|
|
1852
|
+
console.log(c.bold(head));
|
|
1853
|
+
for (const row of rest) {
|
|
1854
|
+
console.log(row === '' ? '' : ` ${wrap(row, 74, ' ')}`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (gate) {
|
|
1858
|
+
if (verification.gateFailures > 0) {
|
|
1859
|
+
console.error(c.red(t.verify.gateFailed(n(verification.gateFailures), n(verification.actions.length))));
|
|
1860
|
+
process.exitCode = 1;
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
console.log(c.green(t.verify.gateOk()));
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1647
1867
|
/**
|
|
1648
1868
|
* `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
|
|
1649
1869
|
*
|
|
@@ -4935,6 +5155,12 @@ async function main() {
|
|
|
4935
5155
|
case 'plan':
|
|
4936
5156
|
await commandPlan(args, pricing, t);
|
|
4937
5157
|
break;
|
|
5158
|
+
case 'verify':
|
|
5159
|
+
await commandVerify(args, pricing, t);
|
|
5160
|
+
break;
|
|
5161
|
+
case 'history':
|
|
5162
|
+
await commandHistory(args, t);
|
|
5163
|
+
break;
|
|
4938
5164
|
case 'route':
|
|
4939
5165
|
await commandRoute(args, pricing, t);
|
|
4940
5166
|
break;
|