@trazum/cli 1.50.2 → 1.50.3

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, 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();
@@ -3984,7 +4074,7 @@ async function commandProfile(args, config, pricing, t) {
3984
4074
  if (waiverUses.length === 0)
3985
4075
  return;
3986
4076
  for (const use of waiverUses) {
3987
- const failed = await appendWaiverUse('.', use);
4077
+ const failed = await appendWaiverUse(configDir, use);
3988
4078
  if (failed !== null) {
3989
4079
  console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
3990
4080
  return;
@@ -6431,6 +6521,22 @@ async function main() {
6431
6521
  };
6432
6522
  }
6433
6523
  const { config } = loaded;
6524
+ /**
6525
+ * Where the waiver record lives: **beside the config that declared it**.
6526
+ *
6527
+ * It used to be the process's working directory, which is a different place
6528
+ * whenever somebody runs `trazum profile ../logs/x.jsonl --config ../repo/
6529
+ * trazum.config.json` — and that is not hypothetical. This repository's own
6530
+ * test suite did exactly that from `packages/cli`, so sixty records of a
6531
+ * fixture's decisions accumulated in a package directory and one of them was
6532
+ * committed to `main`, where it sat for two releases.
6533
+ *
6534
+ * A waiver is a decision a *repository* made. The record of using it belongs
6535
+ * with the file that made it, not with wherever the terminal happened to be.
6536
+ * No config means no waivers, so there is nothing to write and `.` is never
6537
+ * reached.
6538
+ */
6539
+ const configDir = loaded.path === null ? '.' : dirname(loaded.path);
6434
6540
  const pricing = await pricingFor(args, loaded, t);
6435
6541
  // The config only gets to choose the locale when nothing more explicit did.
6436
6542
  if (config.locale && !stringFlag(args, 'locale')) {
@@ -6448,7 +6554,7 @@ async function main() {
6448
6554
  await commandBaseline(args, config, pricing, t, locale);
6449
6555
  break;
6450
6556
  case 'profile':
6451
- await commandProfile(args, config, pricing, t);
6557
+ await commandProfile(args, config, configDir, pricing, t);
6452
6558
  break;
6453
6559
  case 'plan':
6454
6560
  await commandPlan(args, pricing, t);
@@ -6457,7 +6563,7 @@ async function main() {
6457
6563
  await commandVerify(args, pricing, t);
6458
6564
  break;
6459
6565
  case 'history':
6460
- await commandHistory(args, config, pricing, t);
6566
+ await commandHistory(args, config, configDir, pricing, t);
6461
6567
  break;
6462
6568
  case 'connect':
6463
6569
  await commandConnect(args, pricing, t);
@@ -6486,6 +6592,9 @@ async function main() {
6486
6592
  case 'models':
6487
6593
  commandModels(t, pricing);
6488
6594
  break;
6595
+ case 'gateway':
6596
+ await commandGateway(args, config, configDir, pricing, t);
6597
+ break;
6489
6598
  case 'feedback':
6490
6599
  commandFeedback(t);
6491
6600
  break;