@trazum/cli 1.50.5 → 1.50.6

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
@@ -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, 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
@@ -319,6 +319,7 @@ const COMMAND_FLAGS = {
319
319
  conform: ['contract', 'json'],
320
320
  feedback: [],
321
321
  gateway: ['on-cannot-tell', 'port', 'socket', 'pricing', 'pricing-live'],
322
+ ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
322
323
  where: [],
323
324
  rules: [],
324
325
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1665,6 +1666,109 @@ async function commandGateway(args, config, configDir, pricing, t) {
1665
1666
  console.log(` ${c.dim(wrap(t.gateway.policy(policyFlag), 74, ' '))}`);
1666
1667
  console.log();
1667
1668
  }
1669
+ /**
1670
+ * `trazum ladder <log>` — is the ladder saving money, or is it a bill?
1671
+ *
1672
+ * The one number this command exists to print is the **break-even escalation
1673
+ * rate**. "We route to the cheap model first" describes a policy that saves
1674
+ * money and a policy that costs money equally well; only the rate separates
1675
+ * them, and nobody works it out in their head because the shape of the
1676
+ * arithmetic is not obvious — an escalation pays twice, since the cheap
1677
+ * attempt is not refunded.
1678
+ */
1679
+ async function commandLadder(args, config, pricing, t) {
1680
+ const path = args.positional[0];
1681
+ if (path === undefined) {
1682
+ throw new Error(t.errors.missingInputFile());
1683
+ }
1684
+ const report = profileUsage(await readUsageLog(path, t), { catalogue: pricing });
1685
+ const ladders = config.ladders ?? {};
1686
+ const n = (value) => value.toLocaleString(t.numberLocale);
1687
+ const pct = (value) => `${(value * 100).toFixed(1)}%`;
1688
+ console.log();
1689
+ console.log(c.bold(t.ladder.heading()));
1690
+ if (Object.keys(ladders).length === 0) {
1691
+ console.log(` ${c.dim(wrap(t.ladder.noLadders(), 74, ' '))}`);
1692
+ console.log();
1693
+ return;
1694
+ }
1695
+ console.log(` ${c.dim(wrap(t.ladder.theDoubleSpend(), 74, ' '))}`);
1696
+ console.log();
1697
+ const vocabulary = config.outcomes ?? null;
1698
+ let anyProblem = false;
1699
+ for (const [label, policy] of Object.entries(ladders)) {
1700
+ /**
1701
+ * Validated before it is measured, and loudly.
1702
+ *
1703
+ * A ladder that escalates on a value declared a *success* pays twice for
1704
+ * work that already worked, on every call, while looking exactly like a
1705
+ * cost-saving measure in the config. Printing its measured position first
1706
+ * would bury that under a number.
1707
+ */
1708
+ const problems = validateLadder(policy, vocabulary, pricing);
1709
+ if (problems.length > 0) {
1710
+ anyProblem = true;
1711
+ console.log(` ${c.red('✗')} ${c.bold(t.ladder.problemsHeading(label))}`);
1712
+ for (const problem of problems) {
1713
+ const detail = 'value' in problem
1714
+ ? problem.value
1715
+ : 'model' in problem
1716
+ ? problem.model
1717
+ : String(problem.tiers);
1718
+ console.log(` ${wrap(t.ladder.problem(problem.kind, detail), 70, ' ')}`);
1719
+ }
1720
+ console.log();
1721
+ continue;
1722
+ }
1723
+ const slice = report.outcomeTallyByLabel.find((entry) => entry.label === label);
1724
+ const breakdown = report.byLabel.find((entry) => entry.label === label);
1725
+ /**
1726
+ * The shape of the work comes from the measured calls, so the break-even
1727
+ * rate is priced against what this workload actually sends rather than
1728
+ * against a token count somebody guessed at.
1729
+ */
1730
+ const calls = breakdown?.breakdown.calls ?? 0;
1731
+ const shape = breakdown === undefined || calls === 0
1732
+ ? { inputTokens: 0, outputTokens: 0 }
1733
+ : {
1734
+ inputTokens: Math.round((breakdown.breakdown.inputTokens +
1735
+ breakdown.breakdown.cacheReadTokens +
1736
+ breakdown.breakdown.cacheWriteTokens) /
1737
+ calls),
1738
+ outputTokens: Math.round(breakdown.breakdown.outputTokens / calls),
1739
+ };
1740
+ const empty = { byValue: [], recorded: 0, parsed: 0, unrecordedUsd: 0 };
1741
+ const position = ladderPosition(policy, slice?.tally ?? empty, shape, vocabulary, pricing);
1742
+ console.log(` ${c.bold(t.ladder.workload(label))} ${c.dim(policy.tiers.join(' → '))}`);
1743
+ console.log(` ${c.dim(t.ladder.arithmetic(formatUsd(position.arithmetic.cheapUsd), formatUsd(position.arithmetic.dearUsd), position.arithmetic.breakEvenRate === null ? '—' : pct(position.arithmetic.breakEvenRate)))}`);
1744
+ if (position.verdict === 'cannot-tell') {
1745
+ console.log(` ${c.yellow('?')} ${wrap(t.ladder.cannotTell(position.unknown ?? '', n(position.calls)), 70, ' ')}`);
1746
+ }
1747
+ else {
1748
+ console.log(` ${t.ladder.measured(pct(position.measuredRate ?? 0), n(position.escalations), n(position.calls))}`);
1749
+ const delta = formatUsd(Math.abs(position.deltaUsdPerCall ?? 0));
1750
+ if (position.verdict === 'saving') {
1751
+ console.log(` ${c.green('✓')} ${wrap(t.ladder.saving(delta), 70, ' ')}`);
1752
+ }
1753
+ else if (position.verdict === 'costing') {
1754
+ console.log(` ${c.red('✗')} ${wrap(t.ladder.costing(delta), 70, ' ')}`);
1755
+ }
1756
+ else {
1757
+ console.log(` ${c.dim('·')} ${wrap(t.ladder.atBreakEven(pct(BREAK_EVEN_BAND)), 70, ' ')}`);
1758
+ }
1759
+ }
1760
+ console.log();
1761
+ }
1762
+ console.log(` ${c.dim(wrap(t.ladder.notExecuted(), 74, ' '))}`);
1763
+ console.log();
1764
+ /**
1765
+ * A misconfigured ladder fails the command, because it is the one finding
1766
+ * here that is wrong *now* rather than a measurement somebody should look
1767
+ * at. Everything else exits 0: this is a survey, like `doctor`.
1768
+ */
1769
+ if (anyProblem)
1770
+ process.exitCode = 1;
1771
+ }
1668
1772
  function commandModels(t, pricing) {
1669
1773
  const n = (value) => value.toLocaleString(t.numberLocale);
1670
1774
  const col = t.models.columns;
@@ -6712,6 +6816,9 @@ async function main() {
6712
6816
  case 'models':
6713
6817
  commandModels(t, pricing);
6714
6818
  break;
6819
+ case 'ladder':
6820
+ await commandLadder(args, config, pricing, t);
6821
+ break;
6715
6822
  case 'gateway':
6716
6823
  await commandGateway(args, config, configDir, pricing, t);
6717
6824
  break;