@trazum/cli 1.31.0 → 1.33.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
@@ -259,7 +259,7 @@ const COMMAND_FLAGS = {
259
259
  ],
260
260
  check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
261
261
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
262
- 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'],
262
+ 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'],
263
263
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
264
264
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
265
265
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -1626,6 +1626,53 @@ async function commandProfile(args, config, pricing, t) {
1626
1626
  }
1627
1627
  const n = (value) => value.toLocaleString(t.numberLocale);
1628
1628
  const pct = (share) => `${(share * 100).toFixed(1)}%`;
1629
+ /**
1630
+ * `--dry-run`: what this log could and could not answer, and no bill.
1631
+ *
1632
+ * The question somebody has *before* wiring Trazum into CI is not "what did
1633
+ * we spend" but "will this log support the gates I want" — and answering it
1634
+ * with a full report makes them read a bill to find out a field is missing.
1635
+ * This path states readiness per capability and produces no dollar figure
1636
+ * at all, so nothing here can be mistaken for spend. It also refuses to
1637
+ * coexist with the gates: a gate over a report that was never produced
1638
+ * would exit green having judged nothing.
1639
+ */
1640
+ if (boolFlag(args, 'dry-run')) {
1641
+ const gateFlags = ['max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd'];
1642
+ if (gateFlags.some((flag) => typeof args.flags.get(flag) === 'string')) {
1643
+ throw new Error(t.profile.dryRunNoGates());
1644
+ }
1645
+ const cov = report.fieldCoverage;
1646
+ const parsed = cov.parsed;
1647
+ console.log(c.bold(t.profile.dryRunHeading()));
1648
+ console.log(` ${wrap(t.profile.dryRunParsed(n(parsed), n(report.skippedLines.length)), 74, ' ')}`);
1649
+ if (report.unpricedModels.length > 0) {
1650
+ console.log(` ${c.yellow('!')} ${wrap(t.profile.dryRunUnpriced(report.unpricedModels.join(', ')), 74, ' ')}`);
1651
+ }
1652
+ console.log();
1653
+ const can = (ok, line) => {
1654
+ console.log(` ${ok ? c.green('✓') : c.yellow('✗')} ${wrap(line, 74, ' ')}`);
1655
+ };
1656
+ const share = (count) => (parsed > 0 ? pct(count / parsed) : '0%');
1657
+ can(parsed > 0, t.profile.dryRunTotals());
1658
+ can(cov.label > 0, t.profile.dryRunLabels(share(cov.label)));
1659
+ can(cov.ts > 0, t.profile.dryRunClock(share(cov.ts)));
1660
+ can(cov.session > 0, t.profile.dryRunSessions(share(cov.session)));
1661
+ can(cov.stopReason > 0, t.profile.dryRunStopReason(share(cov.stopReason)));
1662
+ // "No cache traffic" is not a missing field: the split can only exist on
1663
+ // records that wrote, and a log that never wrote has nothing to record.
1664
+ if (cov.cacheWrites > 0) {
1665
+ can(cov.cacheTtl > 0, t.profile.dryRunCacheTtl(n(cov.cacheTtl), n(cov.cacheWrites)));
1666
+ }
1667
+ else {
1668
+ console.log(` ${c.dim('·')} ${wrap(t.profile.dryRunNoCacheTraffic(), 74, ' ')}`);
1669
+ }
1670
+ console.log();
1671
+ console.log(` ${c.dim(wrap(t.profile.dryRunFooter(), 74, ' '))}`);
1672
+ if (parsed === 0)
1673
+ process.exitCode = 1;
1674
+ return;
1675
+ }
1629
1676
  /**
1630
1677
  * The previous log, loaded before the output paths split so the growth gate
1631
1678
  * exists under `--json` too — a CI step reads the JSON and trusts the exit
@@ -2717,9 +2764,27 @@ async function commandProfile(args, config, pricing, t) {
2717
2764
  const line = t.profile.whatIfTotal(formatUsd(whatIf.currentUsd), formatUsd(whatIf.targetUsd), formatUsd(Math.abs(whatIf.deltaUsd)));
2718
2765
  console.log(` ${cheaper ? c.green('→') : c.yellow('!')} ${c.bold(wrap(line, 74, ' '))}`);
2719
2766
  console.log(` ${c.dim(wrap(cheaper ? t.profile.whatIfCheaper() : t.profile.whatIfDearer(), 74, ' '))}`);
2767
+ /**
2768
+ * The other half of the whole decision: the same move with the target's
2769
+ * Batch API on top. Computed against the target's rates, never by
2770
+ * adding two savings, and hedged the only honest way — whether these
2771
+ * calls can wait is not in the log.
2772
+ */
2773
+ if (whatIf.batchOnTarget !== null) {
2774
+ console.log(` ${c.dim(wrap(t.profile.whatIfBatchOnTarget(formatUsd(whatIf.batchOnTarget.targetUsd), formatUsd(whatIf.targetUsd)), 74, ' '))}`);
2775
+ }
2720
2776
  for (const slice of whatIf.slices.slice(0, 5)) {
2721
2777
  const label = slice.label === UNLABELLED ? t.profile.unlabelled() : slice.label;
2722
2778
  console.log(` ${c.dim('·')} ${c.dim(wrap(t.profile.whatIfSlice(label, slice.model, formatUsd(slice.currentUsd), formatUsd(slice.targetUsd)), 74, ' '))}`);
2779
+ /**
2780
+ * Cache traffic the target could not grant, said loudly and beside
2781
+ * the figure it corrects. The standard row prices cache entries the
2782
+ * target's minimum would refuse to create — an error that flatters
2783
+ * the move, which is the direction this repository refuses.
2784
+ */
2785
+ if (slice.cacheBeyondTarget !== null) {
2786
+ console.log(` ${c.yellow('!')} ${c.dim(wrap(t.profile.whatIfCacheBeyond(n(slice.maxCallInputTokens), n(slice.cacheBeyondTarget.minTokens), formatUsd(slice.cacheBeyondTarget.noCacheUsd)), 74, ' '))}`);
2787
+ }
2723
2788
  }
2724
2789
  }
2725
2790
  /**