@trazum/cli 1.50.2 → 1.50.4

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, 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, outcomeReport, 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
@@ -15,6 +15,7 @@ import { fetchProviderUsage, findCredential } from './connect.js';
15
15
  import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
16
16
  import { WAIVER_LOG, appendWaiverUse, readWaiverLog } from './waiver-log.js';
17
17
  import { DEFAULT_PORT, buildServer, listen } from './serve.js';
18
+ import { DEFAULT_GATEWAY_PORT, UPSTREAMS, buildGateway, listenGateway, } from './gateway-server.js';
18
19
  import { WATCH_STATE_VERSION, checkWebhook, postWebhook, readWatchState, writeWatchState, } from './watch-run.js';
19
20
  import { LOCALE_ENV_VARS, detectLocale, getCliMessages } from './i18n/index.js';
20
21
  import { MAX_SUMMARY_CHARS, fitWithin, renderBlameMarkdown, renderCheckMarkdown, renderDiffMarkdown, renderRankMarkdown, renderProfileMarkdown, } from './markdown.js';
@@ -33,6 +34,7 @@ const c = {
33
34
  const VALUE_FLAGS = new Set([
34
35
  'against',
35
36
  'contract',
37
+ 'on-cannot-tell',
36
38
  'from-log',
37
39
  'min-usd',
38
40
  'payload',
@@ -316,6 +318,7 @@ const COMMAND_FLAGS = {
316
318
  init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
317
319
  conform: ['contract', 'json'],
318
320
  feedback: [],
321
+ gateway: ['on-cannot-tell', 'port', 'socket', 'pricing', 'pricing-live'],
319
322
  where: [],
320
323
  rules: [],
321
324
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1575,6 +1578,93 @@ function commandFeedback(t) {
1575
1578
  console.log(` ${url}`);
1576
1579
  console.log();
1577
1580
  }
1581
+ /**
1582
+ * `trazum gateway <provider>` — in the path, and refusing rather than advising.
1583
+ *
1584
+ * The last thing this product could not do. `serve` answers a question an
1585
+ * implementation may ignore; a connector reports the runaway after it ran.
1586
+ * Standing between the caller and the provider fixes both — usage is measured
1587
+ * from the provider's own response as it comes back, and a refusal is a
1588
+ * refusal.
1589
+ *
1590
+ * **The failure policy is required.** `--on-cannot-tell fail-open` keeps the
1591
+ * product working and lets the bill run; `fail-closed` stops the bill and takes
1592
+ * the product down with it. Both are defensible and there is deliberately no
1593
+ * default: a proxy that picks silently has made the most consequential decision
1594
+ * in somebody's architecture on their behalf, at install time, without saying
1595
+ * so.
1596
+ *
1597
+ * **Substitution is off unless it is written down.** `spend.substitute` in the
1598
+ * config, with the operator's own reason, and every substituted call is marked
1599
+ * so no later report treats it as the call the caller made.
1600
+ */
1601
+ async function commandGateway(args, config, configDir, pricing, t) {
1602
+ const provider = args.positional[0];
1603
+ if (provider === undefined || UPSTREAMS[provider] === undefined) {
1604
+ throw new Error(t.gateway.badProvider(provider ?? '', Object.keys(UPSTREAMS).join(', ')));
1605
+ }
1606
+ /**
1607
+ * No default, and the error says why rather than just what.
1608
+ *
1609
+ * The one flag in this product that refuses to guess on the reader's behalf,
1610
+ * because the two answers differ in which failure they accept and nobody but
1611
+ * the operator knows which their product can survive.
1612
+ */
1613
+ const policyFlag = stringFlag(args, 'on-cannot-tell');
1614
+ if (policyFlag === undefined || !FAILURE_POLICIES.includes(policyFlag)) {
1615
+ throw new Error(t.gateway.needsPolicy(FAILURE_POLICIES.join(', ')));
1616
+ }
1617
+ const { resolved } = await readStore(configDir);
1618
+ const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
1619
+ const position = budget.positions[0] ?? null;
1620
+ /**
1621
+ * Read once at start, like `serve`'s.
1622
+ *
1623
+ * A file read in the request path would put Trazum's own latency between a
1624
+ * caller and their provider on every call, which is a cost this product
1625
+ * would otherwise be reporting on somebody else. The staleness is real, so a
1626
+ * refusal carries `asOfMs` and says what it rested on.
1627
+ */
1628
+ const standing = position === null || position.coverage === 'none'
1629
+ ? null
1630
+ : {
1631
+ limitUsd: position.limitUsd,
1632
+ consumedUsd: position.consumedUsd,
1633
+ provenance: 'measured',
1634
+ asOfMs: Date.now(),
1635
+ };
1636
+ const measured = { calls: 0, usd: 0 };
1637
+ const server = buildGateway({
1638
+ provider,
1639
+ catalogue: pricing,
1640
+ policy: {
1641
+ onCannotTell: policyFlag,
1642
+ ...(config.spend?.substitute === undefined ? {} : { substitute: config.spend.substitute }),
1643
+ },
1644
+ standing: () => standing,
1645
+ record: (call) => {
1646
+ measured.calls += 1;
1647
+ console.error(c.dim(t.gateway.measured(call.model, call.label, call.inputTokens, call.outputTokens, call.substituted)));
1648
+ },
1649
+ note: (line) => {
1650
+ console.error(c.yellow(` ${line}`));
1651
+ },
1652
+ });
1653
+ const socket = stringFlag(args, 'socket');
1654
+ const portRaw = stringFlag(args, 'port');
1655
+ const port = portRaw === undefined ? DEFAULT_GATEWAY_PORT : Number(portRaw);
1656
+ if (socket === undefined && (!Number.isInteger(port) || port < 0 || port > 65_535)) {
1657
+ throw new Error(t.serve.badPort(String(portRaw)));
1658
+ }
1659
+ const where = await listenGateway(server, socket !== undefined ? { socket } : { port });
1660
+ console.log(c.bold(t.gateway.listening(where, provider)));
1661
+ console.log(` ${c.dim(wrap(t.gateway.pointYourSdk(where), 74, ' '))}`);
1662
+ console.log(` ${c.dim(wrap(t.gateway.credential(), 74, ' '))}`);
1663
+ console.log(` ${c.dim(wrap(t.gateway.neverSubstitutes(), 74, ' '))}`);
1664
+ console.log(` ${c.dim(wrap(standing === null ? t.gateway.noStanding() : t.gateway.standing(formatUsd(standing.consumedUsd), formatUsd(standing.limitUsd)), 74, ' '))}`);
1665
+ console.log(` ${c.dim(wrap(t.gateway.policy(policyFlag), 74, ' '))}`);
1666
+ console.log();
1667
+ }
1578
1668
  function commandModels(t, pricing) {
1579
1669
  const n = (value) => value.toLocaleString(t.numberLocale);
1580
1670
  const col = t.models.columns;
@@ -2761,7 +2851,7 @@ async function commandConnect(args, pricing, t) {
2761
2851
  * same action planned twice — and no series, however long, becomes a
2762
2852
  * forecast.
2763
2853
  */
2764
- async function commandHistory(args, config, pricing, t) {
2854
+ async function commandHistory(args, config, configDir, pricing, t) {
2765
2855
  /**
2766
2856
  * `--store` builds the series from measured spend already on disk.
2767
2857
  *
@@ -2873,7 +2963,7 @@ async function commandHistory(args, config, pricing, t) {
2873
2963
  * the waiver record belongs to the repository whose gates fired, and the
2874
2964
  * stored reports may have come from anywhere.
2875
2965
  */
2876
- const waivers = await readWaiverLog('.');
2966
+ const waivers = await readWaiverLog(configDir);
2877
2967
  const waiverReport = waiverHistory(waivers.uses, config.waive ?? []);
2878
2968
  const stamped = { ...history, unrecognizedFiles: unrecognized, waivers: waiverReport };
2879
2969
  const n = (value) => value.toLocaleString(t.numberLocale);
@@ -3225,7 +3315,7 @@ async function commandPlan(args, pricing, t) {
3225
3315
  if (outPath !== undefined)
3226
3316
  console.log(c.dim(wrap(t.plan.wrote(outPath), 74, '')));
3227
3317
  }
3228
- async function commandProfile(args, config, pricing, t) {
3318
+ async function commandProfile(args, config, configDir, pricing, t) {
3229
3319
  const path = args.positional[0];
3230
3320
  if (path === undefined) {
3231
3321
  console.log();
@@ -3497,6 +3587,7 @@ async function commandProfile(args, config, pricing, t) {
3497
3587
  can(cov.label > 0, t.profile.dryRunLabels(share(cov.label)));
3498
3588
  can(cov.ts > 0, t.profile.dryRunClock(share(cov.ts)));
3499
3589
  can(cov.session > 0, t.profile.dryRunSessions(share(cov.session)));
3590
+ can(cov.outcome > 0, t.profile.dryRunOutcomes(share(cov.outcome)));
3500
3591
  can(cov.stopReason > 0, t.profile.dryRunStopReason(share(cov.stopReason)));
3501
3592
  // "No cache traffic" is not a missing field: the split can only exist on
3502
3593
  // records that wrote, and a log that never wrote has nothing to record.
@@ -3984,7 +4075,7 @@ async function commandProfile(args, config, pricing, t) {
3984
4075
  if (waiverUses.length === 0)
3985
4076
  return;
3986
4077
  for (const use of waiverUses) {
3987
- const failed = await appendWaiverUse('.', use);
4078
+ const failed = await appendWaiverUse(configDir, use);
3988
4079
  if (failed !== null) {
3989
4080
  console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
3990
4081
  return;
@@ -5119,6 +5210,69 @@ async function commandProfile(args, config, pricing, t) {
5119
5210
  * section at all, because a paragraph of things that are fine is the
5120
5211
  * paragraph readers learn to skip.
5121
5212
  */
5213
+ /**
5214
+ * Outcomes — the counterpart, where somebody recorded one.
5215
+ *
5216
+ * Printed above the coverage section rather than below it, because when this
5217
+ * section is present it is the most valuable thing on the page: every other
5218
+ * figure in this report is a cost, and this is the only one that says what
5219
+ * the money bought.
5220
+ *
5221
+ * Silent when nothing recorded an outcome. The coverage section below
5222
+ * already names the missing field and what it would unlock, and printing an
5223
+ * empty Outcomes heading above it would be the same sentence twice.
5224
+ */
5225
+ {
5226
+ const outcomes = outcomeReport(report.outcomeTally, config.outcomes ?? null);
5227
+ if (outcomes.coverage.recorded > 0) {
5228
+ console.log();
5229
+ console.log(c.bold(t.profile.outcomeHeading()));
5230
+ const col = t.profile.outcomeColumns;
5231
+ const rows = [...outcomes.slices, ...outcomes.undeclared].map((slice) => ({
5232
+ value: slice.value,
5233
+ verdict: slice.verdict === 'success'
5234
+ ? t.profile.verdictSuccess()
5235
+ : slice.verdict === 'undeclared'
5236
+ ? t.profile.verdictUndeclared()
5237
+ : t.profile.verdictOther(),
5238
+ calls: n(slice.calls),
5239
+ spend: formatUsd(slice.usd),
5240
+ }));
5241
+ const w = {
5242
+ value: Math.max(...rows.map((r) => r.value.length), col.outcome.length),
5243
+ verdict: Math.max(...rows.map((r) => r.verdict.length), 0),
5244
+ calls: Math.max(...rows.map((r) => r.calls.length), col.calls.length),
5245
+ spend: Math.max(...rows.map((r) => r.spend.length), col.spend.length),
5246
+ };
5247
+ console.log(c.dim(` ${col.outcome.padEnd(w.value)} ${''.padEnd(w.verdict)} ` +
5248
+ `${col.calls.padStart(w.calls)} ${col.spend.padStart(w.spend)}`));
5249
+ for (const row of rows) {
5250
+ const tint = row.verdict === t.profile.verdictUndeclared()
5251
+ ? c.yellow
5252
+ : row.verdict === t.profile.verdictSuccess()
5253
+ ? c.green
5254
+ : c.dim;
5255
+ console.log(` ${row.value.padEnd(w.value)} ${tint(row.verdict.padEnd(w.verdict))} ` +
5256
+ `${row.calls.padStart(w.calls)} ${row.spend.padStart(w.spend)}`);
5257
+ }
5258
+ console.log();
5259
+ if (outcomes.successShareOfRecordedUsd !== null) {
5260
+ const declaredUsd = outcomes.slices.reduce((sum, slice) => sum + slice.usd, 0);
5261
+ console.log(` ${wrap(t.profile.outcomeRate(pct(outcomes.successShareOfRecordedUsd), formatUsd(declaredUsd)), 74, ' ')}`);
5262
+ }
5263
+ else if (outcomes.noRate !== null) {
5264
+ console.log(` ${c.dim(wrap(t.profile.outcomeNoRate(outcomes.noRate), 74, ' '))}`);
5265
+ }
5266
+ // What the rate does not cover, every time it is printed. A rate over a
5267
+ // twelfth of the bill is a rate about a twelfth of the bill.
5268
+ if (outcomes.coverage.unrecordedUsd > 0 && report.total.totalUsd > 0) {
5269
+ console.log(` ${c.yellow('!')} ${wrap(t.profile.outcomeUnrecorded(pct(outcomes.coverage.unrecordedUsd / report.total.totalUsd), formatUsd(outcomes.coverage.unrecordedUsd)), 74, ' ')}`);
5270
+ }
5271
+ if (outcomes.undeclared.length > 0) {
5272
+ console.log(` ${c.yellow('!')} ${wrap(t.profile.outcomeUndeclared(outcomes.undeclared.map((s) => s.value).join(', ')), 74, ' ')}`);
5273
+ }
5274
+ }
5275
+ }
5122
5276
  const coverage = report.fieldCoverage;
5123
5277
  if (coverage.parsed > 0) {
5124
5278
  const missing = [];
@@ -5129,6 +5283,14 @@ async function commandProfile(args, config, pricing, t) {
5129
5283
  if (coverage.session < coverage.parsed) {
5130
5284
  missing.push(t.profile.needsSession(partial(coverage.session)));
5131
5285
  }
5286
+ /**
5287
+ * Listed first among the missing when it is missing entirely, because it
5288
+ * is the one field that changes what every other figure here *means*. The
5289
+ * rest sharpen a cost; this one gives it a counterpart.
5290
+ */
5291
+ if (coverage.outcome < coverage.parsed) {
5292
+ missing.push(t.profile.needsOutcome(partial(coverage.outcome)));
5293
+ }
5132
5294
  if (coverage.ts < coverage.parsed) {
5133
5295
  missing.push(t.profile.needsTs(partial(coverage.ts)));
5134
5296
  }
@@ -6431,6 +6593,22 @@ async function main() {
6431
6593
  };
6432
6594
  }
6433
6595
  const { config } = loaded;
6596
+ /**
6597
+ * Where the waiver record lives: **beside the config that declared it**.
6598
+ *
6599
+ * It used to be the process's working directory, which is a different place
6600
+ * whenever somebody runs `trazum profile ../logs/x.jsonl --config ../repo/
6601
+ * trazum.config.json` — and that is not hypothetical. This repository's own
6602
+ * test suite did exactly that from `packages/cli`, so sixty records of a
6603
+ * fixture's decisions accumulated in a package directory and one of them was
6604
+ * committed to `main`, where it sat for two releases.
6605
+ *
6606
+ * A waiver is a decision a *repository* made. The record of using it belongs
6607
+ * with the file that made it, not with wherever the terminal happened to be.
6608
+ * No config means no waivers, so there is nothing to write and `.` is never
6609
+ * reached.
6610
+ */
6611
+ const configDir = loaded.path === null ? '.' : dirname(loaded.path);
6434
6612
  const pricing = await pricingFor(args, loaded, t);
6435
6613
  // The config only gets to choose the locale when nothing more explicit did.
6436
6614
  if (config.locale && !stringFlag(args, 'locale')) {
@@ -6448,7 +6626,7 @@ async function main() {
6448
6626
  await commandBaseline(args, config, pricing, t, locale);
6449
6627
  break;
6450
6628
  case 'profile':
6451
- await commandProfile(args, config, pricing, t);
6629
+ await commandProfile(args, config, configDir, pricing, t);
6452
6630
  break;
6453
6631
  case 'plan':
6454
6632
  await commandPlan(args, pricing, t);
@@ -6457,7 +6635,7 @@ async function main() {
6457
6635
  await commandVerify(args, pricing, t);
6458
6636
  break;
6459
6637
  case 'history':
6460
- await commandHistory(args, config, pricing, t);
6638
+ await commandHistory(args, config, configDir, pricing, t);
6461
6639
  break;
6462
6640
  case 'connect':
6463
6641
  await commandConnect(args, pricing, t);
@@ -6486,6 +6664,9 @@ async function main() {
6486
6664
  case 'models':
6487
6665
  commandModels(t, pricing);
6488
6666
  break;
6667
+ case 'gateway':
6668
+ await commandGateway(args, config, configDir, pricing, t);
6669
+ break;
6489
6670
  case 'feedback':
6490
6671
  commandFeedback(t);
6491
6672
  break;