@trazum/cli 1.50.5 → 1.50.7
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 +68 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +68 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +32 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +196 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +87 -0
- package/src/i18n/es.ts +87 -0
- package/src/i18n/types.ts +34 -0
- package/src/index.ts +275 -0
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { gunzipSync } from 'node:zlib';
|
|
7
|
-
import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, conform, outcomeReport, rankPerOutcome, FAILURE_POLICIES, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, 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';
|
|
7
|
+
import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, conform, BREAK_EVEN_BAND, runExperiment, ladderPosition, validateLadder, outcomeReport, rankPerOutcome, FAILURE_POLICIES, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, 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';
|
|
8
8
|
import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
|
|
9
9
|
import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
10
10
|
// Everything that reads the filesystem, on its own entry point so the web
|
|
@@ -32,6 +32,9 @@ const c = {
|
|
|
32
32
|
cyan: (s) => (useColor ? `\u001b[36m${s}\u001b[39m` : s),
|
|
33
33
|
};
|
|
34
34
|
const VALUE_FLAGS = new Set([
|
|
35
|
+
'a',
|
|
36
|
+
'b',
|
|
37
|
+
'min-outcomes',
|
|
35
38
|
'against',
|
|
36
39
|
'contract',
|
|
37
40
|
'on-cannot-tell',
|
|
@@ -319,6 +322,8 @@ const COMMAND_FLAGS = {
|
|
|
319
322
|
conform: ['contract', 'json'],
|
|
320
323
|
feedback: [],
|
|
321
324
|
gateway: ['on-cannot-tell', 'port', 'socket', 'pricing', 'pricing-live'],
|
|
325
|
+
ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
|
|
326
|
+
experiment: ['a', 'b', 'min-outcomes', 'pricing', 'pricing-live'],
|
|
322
327
|
where: [],
|
|
323
328
|
rules: [],
|
|
324
329
|
blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
|
|
@@ -1665,6 +1670,190 @@ async function commandGateway(args, config, configDir, pricing, t) {
|
|
|
1665
1670
|
console.log(` ${c.dim(wrap(t.gateway.policy(policyFlag), 74, ' '))}`);
|
|
1666
1671
|
console.log();
|
|
1667
1672
|
}
|
|
1673
|
+
/**
|
|
1674
|
+
* `trazum ladder <log>` — is the ladder saving money, or is it a bill?
|
|
1675
|
+
*
|
|
1676
|
+
* The one number this command exists to print is the **break-even escalation
|
|
1677
|
+
* rate**. "We route to the cheap model first" describes a policy that saves
|
|
1678
|
+
* money and a policy that costs money equally well; only the rate separates
|
|
1679
|
+
* them, and nobody works it out in their head because the shape of the
|
|
1680
|
+
* arithmetic is not obvious — an escalation pays twice, since the cheap
|
|
1681
|
+
* attempt is not refunded.
|
|
1682
|
+
*/
|
|
1683
|
+
async function commandLadder(args, config, pricing, t) {
|
|
1684
|
+
const path = args.positional[0];
|
|
1685
|
+
if (path === undefined) {
|
|
1686
|
+
throw new Error(t.errors.missingInputFile());
|
|
1687
|
+
}
|
|
1688
|
+
const report = profileUsage(await readUsageLog(path, t), { catalogue: pricing });
|
|
1689
|
+
const ladders = config.ladders ?? {};
|
|
1690
|
+
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1691
|
+
const pct = (value) => `${(value * 100).toFixed(1)}%`;
|
|
1692
|
+
console.log();
|
|
1693
|
+
console.log(c.bold(t.ladder.heading()));
|
|
1694
|
+
if (Object.keys(ladders).length === 0) {
|
|
1695
|
+
console.log(` ${c.dim(wrap(t.ladder.noLadders(), 74, ' '))}`);
|
|
1696
|
+
console.log();
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
console.log(` ${c.dim(wrap(t.ladder.theDoubleSpend(), 74, ' '))}`);
|
|
1700
|
+
console.log();
|
|
1701
|
+
const vocabulary = config.outcomes ?? null;
|
|
1702
|
+
let anyProblem = false;
|
|
1703
|
+
for (const [label, policy] of Object.entries(ladders)) {
|
|
1704
|
+
/**
|
|
1705
|
+
* Validated before it is measured, and loudly.
|
|
1706
|
+
*
|
|
1707
|
+
* A ladder that escalates on a value declared a *success* pays twice for
|
|
1708
|
+
* work that already worked, on every call, while looking exactly like a
|
|
1709
|
+
* cost-saving measure in the config. Printing its measured position first
|
|
1710
|
+
* would bury that under a number.
|
|
1711
|
+
*/
|
|
1712
|
+
const problems = validateLadder(policy, vocabulary, pricing);
|
|
1713
|
+
if (problems.length > 0) {
|
|
1714
|
+
anyProblem = true;
|
|
1715
|
+
console.log(` ${c.red('✗')} ${c.bold(t.ladder.problemsHeading(label))}`);
|
|
1716
|
+
for (const problem of problems) {
|
|
1717
|
+
const detail = 'value' in problem
|
|
1718
|
+
? problem.value
|
|
1719
|
+
: 'model' in problem
|
|
1720
|
+
? problem.model
|
|
1721
|
+
: String(problem.tiers);
|
|
1722
|
+
console.log(` ${wrap(t.ladder.problem(problem.kind, detail), 70, ' ')}`);
|
|
1723
|
+
}
|
|
1724
|
+
console.log();
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
const slice = report.outcomeTallyByLabel.find((entry) => entry.label === label);
|
|
1728
|
+
const breakdown = report.byLabel.find((entry) => entry.label === label);
|
|
1729
|
+
/**
|
|
1730
|
+
* The shape of the work comes from the measured calls, so the break-even
|
|
1731
|
+
* rate is priced against what this workload actually sends rather than
|
|
1732
|
+
* against a token count somebody guessed at.
|
|
1733
|
+
*/
|
|
1734
|
+
const calls = breakdown?.breakdown.calls ?? 0;
|
|
1735
|
+
const shape = breakdown === undefined || calls === 0
|
|
1736
|
+
? { inputTokens: 0, outputTokens: 0 }
|
|
1737
|
+
: {
|
|
1738
|
+
inputTokens: Math.round((breakdown.breakdown.inputTokens +
|
|
1739
|
+
breakdown.breakdown.cacheReadTokens +
|
|
1740
|
+
breakdown.breakdown.cacheWriteTokens) /
|
|
1741
|
+
calls),
|
|
1742
|
+
outputTokens: Math.round(breakdown.breakdown.outputTokens / calls),
|
|
1743
|
+
};
|
|
1744
|
+
const empty = { byValue: [], recorded: 0, parsed: 0, unrecordedUsd: 0 };
|
|
1745
|
+
const position = ladderPosition(policy, slice?.tally ?? empty, shape, vocabulary, pricing);
|
|
1746
|
+
console.log(` ${c.bold(t.ladder.workload(label))} ${c.dim(policy.tiers.join(' → '))}`);
|
|
1747
|
+
console.log(` ${c.dim(t.ladder.arithmetic(formatUsd(position.arithmetic.cheapUsd), formatUsd(position.arithmetic.dearUsd), position.arithmetic.breakEvenRate === null ? '—' : pct(position.arithmetic.breakEvenRate)))}`);
|
|
1748
|
+
if (position.verdict === 'cannot-tell') {
|
|
1749
|
+
console.log(` ${c.yellow('?')} ${wrap(t.ladder.cannotTell(position.unknown ?? '', n(position.calls)), 70, ' ')}`);
|
|
1750
|
+
}
|
|
1751
|
+
else {
|
|
1752
|
+
console.log(` ${t.ladder.measured(pct(position.measuredRate ?? 0), n(position.escalations), n(position.calls))}`);
|
|
1753
|
+
const delta = formatUsd(Math.abs(position.deltaUsdPerCall ?? 0));
|
|
1754
|
+
if (position.verdict === 'saving') {
|
|
1755
|
+
console.log(` ${c.green('✓')} ${wrap(t.ladder.saving(delta), 70, ' ')}`);
|
|
1756
|
+
}
|
|
1757
|
+
else if (position.verdict === 'costing') {
|
|
1758
|
+
console.log(` ${c.red('✗')} ${wrap(t.ladder.costing(delta), 70, ' ')}`);
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
console.log(` ${c.dim('·')} ${wrap(t.ladder.atBreakEven(pct(BREAK_EVEN_BAND)), 70, ' ')}`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
console.log();
|
|
1765
|
+
}
|
|
1766
|
+
console.log(` ${c.dim(wrap(t.ladder.notExecuted(), 74, ' '))}`);
|
|
1767
|
+
console.log();
|
|
1768
|
+
/**
|
|
1769
|
+
* A misconfigured ladder fails the command, because it is the one finding
|
|
1770
|
+
* here that is wrong *now* rather than a measurement somebody should look
|
|
1771
|
+
* at. Everything else exits 0: this is a survey, like `doctor`.
|
|
1772
|
+
*/
|
|
1773
|
+
if (anyProblem)
|
|
1774
|
+
process.exitCode = 1;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* `trazum experiment <log> --a <label> --b <label> --min-outcomes <n>`
|
|
1778
|
+
*
|
|
1779
|
+
* Two arms on real traffic, judged on recorded outcomes and cost together.
|
|
1780
|
+
*
|
|
1781
|
+
* `--min-outcomes` is required and that is the entire point of it. A stopping
|
|
1782
|
+
* rule declared after looking at the numbers is not a stopping rule, and
|
|
1783
|
+
* nothing here can stop somebody reading a result early — what it can do is
|
|
1784
|
+
* make the early read **visible to whoever reads the result later**, which is
|
|
1785
|
+
* the part that survives the afternoon.
|
|
1786
|
+
*/
|
|
1787
|
+
async function commandExperiment(args, config, pricing, t) {
|
|
1788
|
+
const path = args.positional[0];
|
|
1789
|
+
if (path === undefined)
|
|
1790
|
+
throw new Error(t.errors.missingInputFile());
|
|
1791
|
+
const aName = stringFlag(args, 'a');
|
|
1792
|
+
const bName = stringFlag(args, 'b');
|
|
1793
|
+
if (aName === undefined || bName === undefined)
|
|
1794
|
+
throw new Error(t.experiment.needsTwo());
|
|
1795
|
+
const minRaw = stringFlag(args, 'min-outcomes');
|
|
1796
|
+
const minOutcomesPerArm = minRaw === undefined ? Number.NaN : Number(minRaw);
|
|
1797
|
+
if (!Number.isInteger(minOutcomesPerArm) || minOutcomesPerArm < 1) {
|
|
1798
|
+
throw new Error(t.experiment.needsRule());
|
|
1799
|
+
}
|
|
1800
|
+
const report = profileUsage(await readUsageLog(path, t), { catalogue: pricing });
|
|
1801
|
+
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1802
|
+
const pct = (value) => `${(value * 100).toFixed(1)}%`;
|
|
1803
|
+
const armOf = (label) => {
|
|
1804
|
+
const slice = report.outcomeTallyByLabel.find((entry) => entry.label === label);
|
|
1805
|
+
return {
|
|
1806
|
+
name: label,
|
|
1807
|
+
totalUsd: slice?.totalUsd ?? 0,
|
|
1808
|
+
tally: slice?.tally ?? { byValue: [], recorded: 0, parsed: 0, unrecordedUsd: 0 },
|
|
1809
|
+
};
|
|
1810
|
+
};
|
|
1811
|
+
const result = runExperiment({ arms: [aName, bName], minOutcomesPerArm }, { a: armOf(aName), b: armOf(bName) }, config.outcomes ?? null);
|
|
1812
|
+
console.log();
|
|
1813
|
+
console.log(c.bold(t.experiment.heading(aName, bName)));
|
|
1814
|
+
console.log();
|
|
1815
|
+
for (const side of [result.a, result.b]) {
|
|
1816
|
+
console.log(` ${t.experiment.arm(side.name, side.rate === null ? '—' : pct(side.rate), n(side.successes), n(side.recorded), side.interval === null ? '—' : `[${pct(side.interval.low)}, ${pct(side.interval.high)}]`)}`);
|
|
1817
|
+
}
|
|
1818
|
+
console.log();
|
|
1819
|
+
if (result.separation === 'not-separable') {
|
|
1820
|
+
console.log(` ${c.dim('·')} ${wrap(t.experiment.notSeparable(result.notSeparable ?? '', result.outcomesNeededPerArm === null ? '—' : n(result.outcomesNeededPerArm)), 74, ' ')}`);
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
const winner = result.separation === 'a-wins' ? result.a.name : result.b.name;
|
|
1824
|
+
const d = result.difference;
|
|
1825
|
+
// Reported as a magnitude: the sign is carried by which arm is named, and
|
|
1826
|
+
// printing "-30.0% to -18.0%" beside "b wins" is two ways of saying the
|
|
1827
|
+
// same thing that a reader has to reconcile.
|
|
1828
|
+
const lo = Math.min(Math.abs(d.low), Math.abs(d.high));
|
|
1829
|
+
const hi = Math.max(Math.abs(d.low), Math.abs(d.high));
|
|
1830
|
+
console.log(` ${c.green('✓')} ${wrap(t.experiment.wins(winner, pct(lo), pct(hi)), 74, ' ')}`);
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* The peek line, printed **whether or not** the arms separated.
|
|
1834
|
+
*
|
|
1835
|
+
* A separable result read too early is still separable and still read too
|
|
1836
|
+
* early. Collapsing the two would hide one of the facts, and it is always
|
|
1837
|
+
* the inconvenient one that goes.
|
|
1838
|
+
*/
|
|
1839
|
+
console.log();
|
|
1840
|
+
if (result.stopping.honoured) {
|
|
1841
|
+
console.log(` ${c.dim(wrap(t.experiment.honoured(n(result.stopping.declared)), 74, ' '))}`);
|
|
1842
|
+
}
|
|
1843
|
+
else {
|
|
1844
|
+
const short = result.stopping.short === result.a.name ? result.a : result.b;
|
|
1845
|
+
console.log(` ${c.yellow('!')} ${wrap(t.experiment.peeked(short.name, n(result.stopping.declared), n(short.recorded)), 74, ' ')}`);
|
|
1846
|
+
}
|
|
1847
|
+
if (result.marginal !== null) {
|
|
1848
|
+
console.log();
|
|
1849
|
+
console.log(` ${wrap(result.marginal.usdPerExtraSuccess !== null
|
|
1850
|
+
? t.experiment.marginalDearer(result.marginal.better, formatUsd(result.marginal.usdPerExtraSuccess))
|
|
1851
|
+
: t.experiment.marginalCheaper(result.marginal.better), 74, ' ')}`);
|
|
1852
|
+
}
|
|
1853
|
+
console.log();
|
|
1854
|
+
console.log(` ${c.dim(wrap(t.experiment.neverPromotes(), 74, ' '))}`);
|
|
1855
|
+
console.log();
|
|
1856
|
+
}
|
|
1668
1857
|
function commandModels(t, pricing) {
|
|
1669
1858
|
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1670
1859
|
const col = t.models.columns;
|
|
@@ -6712,6 +6901,12 @@ async function main() {
|
|
|
6712
6901
|
case 'models':
|
|
6713
6902
|
commandModels(t, pricing);
|
|
6714
6903
|
break;
|
|
6904
|
+
case 'experiment':
|
|
6905
|
+
await commandExperiment(args, config, pricing, t);
|
|
6906
|
+
break;
|
|
6907
|
+
case 'ladder':
|
|
6908
|
+
await commandLadder(args, config, pricing, t);
|
|
6909
|
+
break;
|
|
6715
6910
|
case 'gateway':
|
|
6716
6911
|
await commandGateway(args, config, configDir, pricing, t);
|
|
6717
6912
|
break;
|