@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/src/index.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  DEFAULT_USAGE,
40
40
  budgetPositions,
41
41
  conform,
42
+ FAILURE_POLICIES,
42
43
  detectFromSource,
43
44
  matchLocale,
44
45
  parsePlanDocument,
@@ -133,6 +134,8 @@ import type {
133
134
  import type {
134
135
  BudgetReport,
135
136
  ContractName,
137
+ FailurePolicy,
138
+ GatewayStanding,
136
139
  UsageProfileReport,
137
140
  WaiverUse,
138
141
  InitDecline,
@@ -179,6 +182,12 @@ import { fetchProviderUsage, findCredential } from './connect.js';
179
182
  import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
180
183
  import { WAIVER_LOG, appendWaiverUse, readWaiverLog } from './waiver-log.js';
181
184
  import { DEFAULT_PORT, buildServer, listen } from './serve.js';
185
+ import {
186
+ DEFAULT_GATEWAY_PORT,
187
+ UPSTREAMS,
188
+ buildGateway,
189
+ listenGateway,
190
+ } from './gateway-server.js';
182
191
  import {
183
192
  WATCH_STATE_VERSION,
184
193
  checkWebhook,
@@ -233,6 +242,7 @@ interface Args {
233
242
  const VALUE_FLAGS = new Set([
234
243
  'against',
235
244
  'contract',
245
+ 'on-cannot-tell',
236
246
  'from-log',
237
247
  'min-usd',
238
248
  'payload',
@@ -557,6 +567,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
557
567
  init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
558
568
  conform: ['contract', 'json'],
559
569
  feedback: [],
570
+ gateway: ['on-cannot-tell', 'port', 'socket', 'pricing', 'pricing-live'],
560
571
  where: [],
561
572
  rules: [],
562
573
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -2064,6 +2075,119 @@ function commandFeedback(t: CliMessages): void {
2064
2075
  console.log();
2065
2076
  }
2066
2077
 
2078
+ /**
2079
+ * `trazum gateway <provider>` — in the path, and refusing rather than advising.
2080
+ *
2081
+ * The last thing this product could not do. `serve` answers a question an
2082
+ * implementation may ignore; a connector reports the runaway after it ran.
2083
+ * Standing between the caller and the provider fixes both — usage is measured
2084
+ * from the provider's own response as it comes back, and a refusal is a
2085
+ * refusal.
2086
+ *
2087
+ * **The failure policy is required.** `--on-cannot-tell fail-open` keeps the
2088
+ * product working and lets the bill run; `fail-closed` stops the bill and takes
2089
+ * the product down with it. Both are defensible and there is deliberately no
2090
+ * default: a proxy that picks silently has made the most consequential decision
2091
+ * in somebody's architecture on their behalf, at install time, without saying
2092
+ * so.
2093
+ *
2094
+ * **Substitution is off unless it is written down.** `spend.substitute` in the
2095
+ * config, with the operator's own reason, and every substituted call is marked
2096
+ * so no later report treats it as the call the caller made.
2097
+ */
2098
+ async function commandGateway(
2099
+ args: Args,
2100
+ config: TrazumConfig,
2101
+ configDir: string,
2102
+ pricing: PricingCatalogue,
2103
+ t: CliMessages,
2104
+ ): Promise<void> {
2105
+ const provider = args.positional[0];
2106
+ if (provider === undefined || UPSTREAMS[provider] === undefined) {
2107
+ throw new Error(t.gateway.badProvider(provider ?? '', Object.keys(UPSTREAMS).join(', ')));
2108
+ }
2109
+
2110
+ /**
2111
+ * No default, and the error says why rather than just what.
2112
+ *
2113
+ * The one flag in this product that refuses to guess on the reader's behalf,
2114
+ * because the two answers differ in which failure they accept and nobody but
2115
+ * the operator knows which their product can survive.
2116
+ */
2117
+ const policyFlag = stringFlag(args, 'on-cannot-tell');
2118
+ if (policyFlag === undefined || !FAILURE_POLICIES.includes(policyFlag as FailurePolicy)) {
2119
+ throw new Error(t.gateway.needsPolicy(FAILURE_POLICIES.join(', ')));
2120
+ }
2121
+
2122
+ const { resolved } = await readStore(configDir);
2123
+ const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
2124
+ const position = budget.positions[0] ?? null;
2125
+
2126
+ /**
2127
+ * Read once at start, like `serve`'s.
2128
+ *
2129
+ * A file read in the request path would put Trazum's own latency between a
2130
+ * caller and their provider on every call, which is a cost this product
2131
+ * would otherwise be reporting on somebody else. The staleness is real, so a
2132
+ * refusal carries `asOfMs` and says what it rested on.
2133
+ */
2134
+ const standing: GatewayStanding | null =
2135
+ position === null || position.coverage === 'none'
2136
+ ? null
2137
+ : {
2138
+ limitUsd: position.limitUsd,
2139
+ consumedUsd: position.consumedUsd,
2140
+ provenance: 'measured',
2141
+ asOfMs: Date.now(),
2142
+ };
2143
+
2144
+ const measured: { calls: number; usd: number } = { calls: 0, usd: 0 };
2145
+ const server = buildGateway({
2146
+ provider,
2147
+ catalogue: pricing,
2148
+ policy: {
2149
+ onCannotTell: policyFlag as FailurePolicy,
2150
+ ...(config.spend?.substitute === undefined ? {} : { substitute: config.spend.substitute }),
2151
+ },
2152
+ standing: () => standing,
2153
+ record: (call) => {
2154
+ measured.calls += 1;
2155
+ console.error(
2156
+ c.dim(
2157
+ t.gateway.measured(
2158
+ call.model,
2159
+ call.label,
2160
+ call.inputTokens,
2161
+ call.outputTokens,
2162
+ call.substituted,
2163
+ ),
2164
+ ),
2165
+ );
2166
+ },
2167
+ note: (line) => {
2168
+ console.error(c.yellow(` ${line}`));
2169
+ },
2170
+ });
2171
+
2172
+ const socket = stringFlag(args, 'socket');
2173
+ const portRaw = stringFlag(args, 'port');
2174
+ const port = portRaw === undefined ? DEFAULT_GATEWAY_PORT : Number(portRaw);
2175
+ if (socket === undefined && (!Number.isInteger(port) || port < 0 || port > 65_535)) {
2176
+ throw new Error(t.serve.badPort(String(portRaw)));
2177
+ }
2178
+
2179
+ const where = await listenGateway(server, socket !== undefined ? { socket } : { port });
2180
+ console.log(c.bold(t.gateway.listening(where, provider)));
2181
+ console.log(` ${c.dim(wrap(t.gateway.pointYourSdk(where), 74, ' '))}`);
2182
+ console.log(` ${c.dim(wrap(t.gateway.credential(), 74, ' '))}`);
2183
+ console.log(` ${c.dim(wrap(t.gateway.neverSubstitutes(), 74, ' '))}`);
2184
+ console.log(
2185
+ ` ${c.dim(wrap(standing === null ? t.gateway.noStanding() : t.gateway.standing(formatUsd(standing.consumedUsd), formatUsd(standing.limitUsd)), 74, ' '))}`,
2186
+ );
2187
+ console.log(` ${c.dim(wrap(t.gateway.policy(policyFlag), 74, ' '))}`);
2188
+ console.log();
2189
+ }
2190
+
2067
2191
  function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
2068
2192
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
2069
2193
  const col = t.models.columns;
@@ -3640,6 +3764,7 @@ async function commandConnect(
3640
3764
  async function commandHistory(
3641
3765
  args: Args,
3642
3766
  config: TrazumConfig,
3767
+ configDir: string,
3643
3768
  pricing: PricingCatalogue,
3644
3769
  t: CliMessages,
3645
3770
  ): Promise<void> {
@@ -3759,7 +3884,7 @@ async function commandHistory(
3759
3884
  * the waiver record belongs to the repository whose gates fired, and the
3760
3885
  * stored reports may have come from anywhere.
3761
3886
  */
3762
- const waivers = await readWaiverLog('.');
3887
+ const waivers = await readWaiverLog(configDir);
3763
3888
  const waiverReport = waiverHistory(waivers.uses, config.waive ?? []);
3764
3889
 
3765
3890
  const stamped = { ...history, unrecognizedFiles: unrecognized, waivers: waiverReport };
@@ -4161,7 +4286,13 @@ async function commandPlan(
4161
4286
  if (outPath !== undefined) console.log(c.dim(wrap(t.plan.wrote(outPath), 74, '')));
4162
4287
  }
4163
4288
 
4164
- async function commandProfile(args: Args, config: TrazumConfig, pricing: PricingCatalogue, t: CliMessages): Promise<void> {
4289
+ async function commandProfile(
4290
+ args: Args,
4291
+ config: TrazumConfig,
4292
+ configDir: string,
4293
+ pricing: PricingCatalogue,
4294
+ t: CliMessages,
4295
+ ): Promise<void> {
4165
4296
  const path = args.positional[0];
4166
4297
  if (path === undefined) {
4167
4298
  console.log();
@@ -4985,7 +5116,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
4985
5116
  const recordWaiverUses = async (): Promise<void> => {
4986
5117
  if (waiverUses.length === 0) return;
4987
5118
  for (const use of waiverUses) {
4988
- const failed = await appendWaiverUse('.', use);
5119
+ const failed = await appendWaiverUse(configDir, use);
4989
5120
  if (failed !== null) {
4990
5121
  console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
4991
5122
  return;
@@ -8014,6 +8145,22 @@ async function main(): Promise<void> {
8014
8145
  };
8015
8146
  }
8016
8147
  const { config } = loaded;
8148
+ /**
8149
+ * Where the waiver record lives: **beside the config that declared it**.
8150
+ *
8151
+ * It used to be the process's working directory, which is a different place
8152
+ * whenever somebody runs `trazum profile ../logs/x.jsonl --config ../repo/
8153
+ * trazum.config.json` — and that is not hypothetical. This repository's own
8154
+ * test suite did exactly that from `packages/cli`, so sixty records of a
8155
+ * fixture's decisions accumulated in a package directory and one of them was
8156
+ * committed to `main`, where it sat for two releases.
8157
+ *
8158
+ * A waiver is a decision a *repository* made. The record of using it belongs
8159
+ * with the file that made it, not with wherever the terminal happened to be.
8160
+ * No config means no waivers, so there is nothing to write and `.` is never
8161
+ * reached.
8162
+ */
8163
+ const configDir = loaded.path === null ? '.' : dirname(loaded.path);
8017
8164
  const pricing = await pricingFor(args, loaded, t);
8018
8165
 
8019
8166
  // The config only gets to choose the locale when nothing more explicit did.
@@ -8033,7 +8180,7 @@ async function main(): Promise<void> {
8033
8180
  await commandBaseline(args, config, pricing, t, locale);
8034
8181
  break;
8035
8182
  case 'profile':
8036
- await commandProfile(args, config, pricing, t);
8183
+ await commandProfile(args, config, configDir, pricing, t);
8037
8184
  break;
8038
8185
  case 'plan':
8039
8186
  await commandPlan(args, pricing, t);
@@ -8042,7 +8189,7 @@ async function main(): Promise<void> {
8042
8189
  await commandVerify(args, pricing, t);
8043
8190
  break;
8044
8191
  case 'history':
8045
- await commandHistory(args, config, pricing, t);
8192
+ await commandHistory(args, config, configDir, pricing, t);
8046
8193
  break;
8047
8194
  case 'connect':
8048
8195
  await commandConnect(args, pricing, t);
@@ -8071,6 +8218,9 @@ async function main(): Promise<void> {
8071
8218
  case 'models':
8072
8219
  commandModels(t, pricing);
8073
8220
  break;
8221
+ case 'gateway':
8222
+ await commandGateway(args, config, configDir, pricing, t);
8223
+ break;
8074
8224
  case 'feedback':
8075
8225
  commandFeedback(t);
8076
8226
  break;