rightmodeler 0.2.1 → 0.3.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.
@@ -0,0 +1,89 @@
1
+ const DATED_SNAPSHOT = /^\d{2,4}(?:-?\d{2}){1,2}$/;
2
+ const PORTKEY_CACHE_HITS = new Set(["hit", "semantic hit"]);
3
+ function objectOf(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value)
5
+ ? value
6
+ : undefined;
7
+ }
8
+ function nonEmptyString(value) {
9
+ return typeof value === "string" && value.length > 0 ? value : undefined;
10
+ }
11
+ export function servedModel(body) {
12
+ return nonEmptyString(objectOf(body)?.model);
13
+ }
14
+ function modelParts(id) {
15
+ const segments = id.toLowerCase().split("/");
16
+ return {
17
+ name: segments[segments.length - 1],
18
+ ...(segments.length > 1 ? { vendor: segments[segments.length - 2] } : {}),
19
+ };
20
+ }
21
+ export function sameModel(requested, served) {
22
+ const want = modelParts(requested);
23
+ const got = modelParts(served);
24
+ if (want.vendor !== undefined &&
25
+ got.vendor !== undefined &&
26
+ want.vendor !== got.vendor) {
27
+ return false;
28
+ }
29
+ if (got.name === want.name)
30
+ return true;
31
+ const prefix = `${want.name}-`;
32
+ return (got.name.startsWith(prefix) &&
33
+ DATED_SNAPSHOT.test(got.name.slice(prefix.length)));
34
+ }
35
+ function header(headers, name) {
36
+ if (headers instanceof Headers)
37
+ return headers.get(name) ?? undefined;
38
+ const value = headers[name];
39
+ return typeof value === "string" ? value : value?.join(", ");
40
+ }
41
+ function substitution(kind, evidence) {
42
+ return { kind, evidence: evidence.slice(0, 200) };
43
+ }
44
+ export function responseSubstitution(input) {
45
+ const body = objectOf(input.body);
46
+ const extra = objectOf(body?.extra_fields);
47
+ const served = servedModel(body);
48
+ if (served !== undefined && !sameModel(input.requestedModel, served)) {
49
+ return substitution("model", `served ${served} for requested ${input.requestedModel}`);
50
+ }
51
+ const fallback = nonEmptyString(objectOf(extra?.routing_info)?.server_side_fallback_model);
52
+ if (fallback !== undefined) {
53
+ return substitution("model", `bifrost server-side fallback served ${fallback}`);
54
+ }
55
+ const cacheStatus = header(input.headers, "x-portkey-cache-status");
56
+ if (cacheStatus !== undefined &&
57
+ PORTKEY_CACHE_HITS.has(cacheStatus.toLowerCase())) {
58
+ return substitution("cache", `x-portkey-cache-status: ${cacheStatus}`);
59
+ }
60
+ const cacheDebug = objectOf(extra?.cache_debug);
61
+ if (cacheDebug?.cache_hit === true) {
62
+ const hitType = cacheDebug.hit_type;
63
+ return substitution("cache", `bifrost cache hit${typeof hitType === "string" ? ` (${hitType})` : ""}`);
64
+ }
65
+ const hookResults = objectOf(body?.hook_results);
66
+ const transformed = [
67
+ hookResults?.before_request_hooks,
68
+ hookResults?.after_request_hooks,
69
+ ]
70
+ .flatMap((hooks) => (Array.isArray(hooks) ? hooks : []))
71
+ .map(objectOf)
72
+ .find((hook) => hook?.transformed === true);
73
+ if (transformed !== undefined) {
74
+ const id = nonEmptyString(transformed.id) ?? "unnamed";
75
+ return substitution("request", `portkey hook ${id} transformed the call`);
76
+ }
77
+ const dropped = [
78
+ extra?.dropped_compat_plugin_params,
79
+ extra?.dropped_unsupported_tools,
80
+ ].flatMap((items) => (Array.isArray(items) ? items : []));
81
+ if (dropped.length > 0) {
82
+ return substitution("request", `bifrost dropped ${dropped.join(", ")}`);
83
+ }
84
+ const converted = nonEmptyString(extra?.converted_request_type);
85
+ if (converted !== undefined) {
86
+ return substitution("request", `bifrost converted the request to ${converted}`);
87
+ }
88
+ return undefined;
89
+ }
@@ -30,6 +30,11 @@ const heartbeat = setInterval(
30
30
  500,
31
31
  );
32
32
  heartbeat.unref();
33
+ const deadlineMs = Number(process.env.RM_DEADLINE_MS ?? 0);
34
+ let active = null;
35
+ if (deadlineMs > 0) {
36
+ setTimeout(() => active?.kill("SIGKILL"), deadlineMs).unref();
37
+ }
33
38
 
34
39
  function start(command, stdoutPath, stderrPath, flags = "w") {
35
40
  const stdout = createWriteStream(stdoutPath, { flags });
@@ -38,6 +43,7 @@ function start(command, stdoutPath, stderrPath, flags = "w") {
38
43
  cwd: APP_ROOT,
39
44
  stdio: ["ignore", "pipe", "pipe"],
40
45
  });
46
+ active = child;
41
47
  child.stdout.pipe(stdout);
42
48
  child.stderr.pipe(stderr);
43
49
  const closed = new Promise((resolve, reject) => {
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { PassThrough } from "node:stream";
13
13
 
14
14
  import { hopByHopHeaders } from "./headers.js";
15
+ import { responseSubstitution, servedModel } from "../provenance.js";
15
16
  import { classifyStream } from "../transport/stream.js";
16
17
 
17
18
  const maxRequestBytes = 10 * 1024 * 1024;
@@ -69,7 +70,32 @@ function parseConfig() {
69
70
  "RM_PRICING_TABLE values must contain non-negative input and output prices",
70
71
  );
71
72
  }
72
- pricingTable[model] = { input: pricing.input, output: pricing.output };
73
+ if (
74
+ pricing.maxOutputTokens !== undefined &&
75
+ (!Number.isSafeInteger(pricing.maxOutputTokens) ||
76
+ pricing.maxOutputTokens <= 0)
77
+ ) {
78
+ throw new Error(
79
+ "RM_PRICING_TABLE maxOutputTokens must be a positive integer",
80
+ );
81
+ }
82
+ pricingTable[model] = {
83
+ input: pricing.input,
84
+ output: pricing.output,
85
+ ...(pricing.maxOutputTokens === undefined
86
+ ? {}
87
+ : { maxOutputTokens: pricing.maxOutputTokens }),
88
+ };
89
+ }
90
+
91
+ const defaultMaxOutputTokens = Number(
92
+ requiredEnv("RM_DEFAULT_MAX_OUTPUT_TOKENS"),
93
+ );
94
+ if (
95
+ !Number.isSafeInteger(defaultMaxOutputTokens) ||
96
+ defaultMaxOutputTokens <= 0
97
+ ) {
98
+ throw new Error("RM_DEFAULT_MAX_OUTPUT_TOKENS must be a positive integer");
73
99
  }
74
100
 
75
101
  const lease = jsonEnv("RM_BUDGET_LEASE");
@@ -88,6 +114,21 @@ function parseConfig() {
88
114
  throw new Error("RM_EGRESS_URL must use http or https");
89
115
  }
90
116
 
117
+ const requestHeaders =
118
+ process.env.RM_REQUEST_HEADERS === undefined
119
+ ? {}
120
+ : jsonEnv("RM_REQUEST_HEADERS");
121
+ if (
122
+ !isObject(requestHeaders) ||
123
+ Object.entries(requestHeaders).some(
124
+ ([name, value]) => name.length === 0 || typeof value !== "string",
125
+ )
126
+ ) {
127
+ throw new Error(
128
+ "RM_REQUEST_HEADERS must map header names to string values",
129
+ );
130
+ }
131
+
91
132
  return {
92
133
  runId: requiredEnv("RM_RUN_ID"),
93
134
  caseId: requiredEnv("RM_CASE_ID"),
@@ -98,7 +139,9 @@ function parseConfig() {
98
139
  egressUrl,
99
140
  swapPolicy,
100
141
  pricingTable,
142
+ defaultMaxOutputTokens,
101
143
  lease,
144
+ requestHeaders,
102
145
  };
103
146
  }
104
147
 
@@ -222,12 +265,13 @@ function responseHeaders(headers) {
222
265
  return forwarded;
223
266
  }
224
267
 
225
- function requestHeaders(headers, bodyLength) {
268
+ function requestHeaders(headers, bodyLength, configured) {
226
269
  const forwarded = {};
227
270
  for (const [name, value] of Object.entries(headers)) {
228
271
  if (
229
272
  value !== undefined &&
230
273
  !hopByHopHeaders.has(name) &&
274
+ name !== "accept-encoding" &&
231
275
  name !== "authorization" &&
232
276
  name !== "host" &&
233
277
  name !== "content-length"
@@ -235,6 +279,8 @@ function requestHeaders(headers, bodyLength) {
235
279
  forwarded[name] = value;
236
280
  }
237
281
  }
282
+ Object.assign(forwarded, configured);
283
+ forwarded["accept-encoding"] = "identity";
238
284
  forwarded["content-length"] = String(bodyLength);
239
285
  return forwarded;
240
286
  }
@@ -288,7 +334,11 @@ function requestUpstream(
288
334
  return new Promise((resolve, reject) => {
289
335
  const request = send(
290
336
  url,
291
- { method, path: requestTarget, headers },
337
+ {
338
+ method,
339
+ path: `${url.pathname.replace(/\/$/, "")}${requestTarget}`,
340
+ headers,
341
+ },
292
342
  (response) => {
293
343
  clearTimeout(deadline);
294
344
  resolve(response);
@@ -437,6 +487,7 @@ async function forwardStreaming(upstream, outgoing, status, spoolSink) {
437
487
  spoolPath: result.spoolPath ?? null,
438
488
  finishedWithoutSentinel: result.finishedWithoutSentinel === true,
439
489
  upstreamFailed,
490
+ model: result.model ?? null,
440
491
  };
441
492
  }
442
493
 
@@ -489,6 +540,7 @@ async function forwardNonStreaming(upstream, outgoing, status) {
489
540
  streamOutcome: "completed",
490
541
  usage: isObject(body) ? (body.usage ?? null) : null,
491
542
  upstreamFailed: false,
543
+ body,
492
544
  };
493
545
  } catch {
494
546
  return { streamOutcome: "truncated", usage: null, upstreamFailed: false };
@@ -624,13 +676,14 @@ async function main() {
624
676
  return;
625
677
  }
626
678
 
627
- const maxTokens = parsed?.max_tokens;
679
+ const requestedLimit =
680
+ parsed?.max_completion_tokens ?? parsed?.max_tokens ?? undefined;
628
681
  if (
629
682
  !isObject(parsed) ||
630
683
  typeof parsed.model !== "string" ||
631
684
  parsed.model.length === 0 ||
632
- !Number.isSafeInteger(maxTokens) ||
633
- maxTokens < 0
685
+ (requestedLimit !== undefined &&
686
+ (!Number.isSafeInteger(requestedLimit) || requestedLimit < 0))
634
687
  ) {
635
688
  recordLost({
636
689
  attemptGroup,
@@ -640,13 +693,26 @@ async function main() {
640
693
  startedAt,
641
694
  });
642
695
  sendJson(outgoing, 400, {
643
- error: "Request body requires model and a non-negative max_tokens.",
696
+ error:
697
+ "Request body requires model; max_completion_tokens or max_tokens must be a non-negative integer when present.",
644
698
  });
645
699
  return;
646
700
  }
647
701
 
702
+ const swapped = stepId in config.swapPolicy;
648
703
  const model = config.swapPolicy[stepId] ?? parsed.model;
649
- const rewritten = { ...parsed, model };
704
+ const rewritten = {
705
+ ...parsed,
706
+ model,
707
+ ...(parsed.stream === true
708
+ ? {
709
+ stream_options: {
710
+ ...(isObject(parsed.stream_options) ? parsed.stream_options : {}),
711
+ include_usage: true,
712
+ },
713
+ }
714
+ : {}),
715
+ };
650
716
  const forwardedBody = Buffer.from(JSON.stringify(rewritten));
651
717
  const pricing = config.pricingTable[model];
652
718
  if (pricing === undefined) {
@@ -662,6 +728,10 @@ async function main() {
662
728
  });
663
729
  return;
664
730
  }
731
+ const maxTokens =
732
+ requestedLimit ??
733
+ pricing.maxOutputTokens ??
734
+ config.defaultMaxOutputTokens;
665
735
 
666
736
  const estimatedInputTokens = forwardedBody.length;
667
737
  const estimatedWorstCaseUsd =
@@ -726,21 +796,25 @@ async function main() {
726
796
  upstreamSource: null,
727
797
  upstreamFailed: false,
728
798
  };
799
+ let provenance = {};
729
800
  try {
730
801
  const upstream = await requestUpstream(
731
802
  config.egressUrl,
732
803
  incoming.url ?? "/",
733
804
  incoming.method ?? "POST",
734
- requestHeaders(incoming.headers, forwardedBody.length),
805
+ requestHeaders(
806
+ incoming.headers,
807
+ forwardedBody.length,
808
+ config.requestHeaders,
809
+ ),
735
810
  forwardedBody,
736
811
  streamHardDeadlineMs,
737
812
  );
738
813
  const status = upstream.statusCode ?? 502;
739
- const declaredSource = upstream.headers[egressSourceHeader];
740
814
  const upstreamSource =
741
- declaredSource === "provider" || declaredSource === "egress"
742
- ? declaredSource
743
- : null;
815
+ upstream.headers[egressSourceHeader] === "egress"
816
+ ? "egress"
817
+ : "provider";
744
818
  const forwarded =
745
819
  rewritten.stream === true && status < 400
746
820
  ? await forwardStreaming(upstream, outgoing, status, {
@@ -755,6 +829,23 @@ async function main() {
755
829
  upstreamStatus: status,
756
830
  upstreamSource: forwarded.upstreamFailed ? "egress" : upstreamSource,
757
831
  };
832
+ if (swapped && status < 400) {
833
+ const answered =
834
+ forwarded.body ??
835
+ (typeof forwarded.model === "string"
836
+ ? { model: forwarded.model }
837
+ : undefined);
838
+ const served = servedModel(answered);
839
+ const substitution = responseSubstitution({
840
+ requestedModel: model,
841
+ headers: upstream.headers,
842
+ body: answered,
843
+ });
844
+ provenance = {
845
+ ...(served === undefined ? {} : { servedModel: served }),
846
+ ...(substitution === undefined ? {} : { substitution }),
847
+ };
848
+ }
758
849
  } catch {
759
850
  if (!outgoing.headersSent) {
760
851
  sendJson(outgoing, 502, { error: "Egress request failed." });
@@ -790,6 +881,7 @@ async function main() {
790
881
  ...(result.finishedWithoutSentinel
791
882
  ? { finishedWithoutSentinel: true }
792
883
  : {}),
884
+ ...provenance,
793
885
  usage,
794
886
  responseSpoolPath: result.spoolPath,
795
887
  costUsd: leaseChargeUsd,
@@ -65,13 +65,13 @@ function parseUsage(value) {
65
65
  if (!isRecord(value) ||
66
66
  typeof value.prompt_tokens !== "number" ||
67
67
  typeof value.completion_tokens !== "number" ||
68
- typeof value.total_tokens !== "number") {
68
+ (value.total_tokens !== undefined && typeof value.total_tokens !== "number")) {
69
69
  return null;
70
70
  }
71
71
  return {
72
72
  inputTokens: value.prompt_tokens,
73
73
  outputTokens: value.completion_tokens,
74
- totalTokens: value.total_tokens,
74
+ totalTokens: value.total_tokens ?? value.prompt_tokens + value.completion_tokens,
75
75
  };
76
76
  }
77
77
  function parseEvent(data) {
@@ -121,6 +121,9 @@ function parseEvent(data) {
121
121
  usage: value.usage === undefined || value.usage === null
122
122
  ? null
123
123
  : parseUsage(value.usage),
124
+ ...(typeof value.model === "string" && value.model.length > 0
125
+ ? { model: value.model }
126
+ : {}),
124
127
  };
125
128
  }
126
129
  function eventData(event) {
@@ -144,6 +147,7 @@ export async function classifyStream(byteStream, options) {
144
147
  const collector = new ContentCollector(options.spoolSink, processingController.signal);
145
148
  let chunks = 0;
146
149
  let usage = null;
150
+ let model;
147
151
  let sawFinish = false;
148
152
  let selectedResult;
149
153
  const result = (outcome, reason, finishedWithoutSentinel = false) => {
@@ -154,6 +158,7 @@ export async function classifyStream(byteStream, options) {
154
158
  usage,
155
159
  chunks,
156
160
  ...(finishedWithoutSentinel ? { finishedWithoutSentinel: true } : {}),
161
+ ...(model === undefined ? {} : { model }),
157
162
  };
158
163
  return selectedResult;
159
164
  };
@@ -283,6 +288,7 @@ export async function classifyStream(byteStream, options) {
283
288
  throw error;
284
289
  }
285
290
  usage = event.usage ?? usage;
291
+ model ??= event.model;
286
292
  sawFinish ||= event.finished;
287
293
  }
288
294
  }
package/docs/commands.md CHANGED
@@ -38,8 +38,9 @@ Commands:
38
38
  rollback [options] open a draft pull request restoring a prior model swap
39
39
  drift [options] detect drift against the active replay corpus
40
40
  watch [options] reconcile one open model-swap pull request
41
- report write report.md and report.json
41
+ report [options] write report.md and report.json
42
42
  status [options] summarize the current store
43
+ docs [name] print documentation packaged with this CLI
43
44
  help [command] display help for command
44
45
  ```
45
46
 
@@ -51,7 +52,8 @@ Usage: rightmodeler init [options]
51
52
  run the resumable Phase A pipeline
52
53
 
53
54
  Options:
54
- --traces <path> trace input file
55
+ --traces <path> trace input file or directory
56
+ --matchers <path> declarative matcher definitions JSON file
55
57
  --include-free include zero-priced models in candidate
56
58
  shortlists
57
59
  --modeb-config <path> versioned Mode B runtime configuration
@@ -62,6 +64,17 @@ Options:
62
64
  --max-cost-usd <amount> optional hard spend cap in USD; omit to
63
65
  run uncapped so every case and judge cell
64
66
  completes
67
+ --max-concurrency <n> maximum concurrent provider requests
68
+ --pricing-file <path> JSON map from model id to per-token input
69
+ and output USD, for catalogs without
70
+ pricing
71
+ --header <header> extra HTTP header for every provider
72
+ request, as 'name: value' (repeatable)
73
+ --catalog-reference <url-or-path> upstream /models URL or file that fills
74
+ pricing, context and capabilities the
75
+ provider catalog lacks
76
+ --policy <path> release policy JSON file: quality floor,
77
+ shortlist size, model allow and deny lists
65
78
  --evaluator <provider> external evaluator provider (choices:
66
79
  "braintrust", "langfuse", "langsmith",
67
80
  "promptfoo")
@@ -84,6 +97,8 @@ Options:
84
97
  "ingest", "reconcile", "scrub", "corpus",
85
98
  "audit-sample", "shortlist", "replay",
86
99
  "aggregate", "confirm", "report")
100
+ --code-graph <path> Graphify graph.json for static code
101
+ context in the report; never evidence
87
102
  --yes accept the newest discovered trace without
88
103
  prompting
89
104
  -h, --help display help for command
@@ -97,7 +112,8 @@ Usage: rightmodeler estimate [options]
97
112
  project replay spend before paid model calls
98
113
 
99
114
  Options:
100
- --traces <path> trace input file
115
+ --traces <path> trace input file or directory
116
+ --matchers <path> declarative matcher definitions JSON file
101
117
  --include-free include zero-priced models in candidate
102
118
  shortlists
103
119
  --modeb-config <path> versioned Mode B runtime configuration
@@ -108,6 +124,17 @@ Options:
108
124
  --max-cost-usd <amount> optional hard spend cap in USD; omit to
109
125
  run uncapped so every case and judge cell
110
126
  completes
127
+ --max-concurrency <n> maximum concurrent provider requests
128
+ --pricing-file <path> JSON map from model id to per-token input
129
+ and output USD, for catalogs without
130
+ pricing
131
+ --header <header> extra HTTP header for every provider
132
+ request, as 'name: value' (repeatable)
133
+ --catalog-reference <url-or-path> upstream /models URL or file that fills
134
+ pricing, context and capabilities the
135
+ provider catalog lacks
136
+ --policy <path> release policy JSON file: quality floor,
137
+ shortlist size, model allow and deny lists
111
138
  --evaluator <provider> external evaluator provider (choices:
112
139
  "braintrust", "langfuse", "langsmith",
113
140
  "promptfoo")
@@ -140,7 +167,8 @@ Usage: rightmodeler scan [options]
140
167
  run through the scan stage
141
168
 
142
169
  Options:
143
- --traces <path> trace input file
170
+ --traces <path> trace input file or directory
171
+ --matchers <path> declarative matcher definitions JSON file
144
172
  --include-free include zero-priced models in candidate shortlists
145
173
  --modeb-config <path> versioned Mode B runtime configuration JSON file
146
174
  -h, --help display help for command
@@ -154,7 +182,8 @@ Usage: rightmodeler ingest [options]
154
182
  run through the ingest stage
155
183
 
156
184
  Options:
157
- --traces <path> trace input file
185
+ --traces <path> trace input file or directory
186
+ --matchers <path> declarative matcher definitions JSON file
158
187
  --include-free include zero-priced models in candidate shortlists
159
188
  --modeb-config <path> versioned Mode B runtime configuration JSON file
160
189
  -h, --help display help for command
@@ -168,7 +197,8 @@ Usage: rightmodeler reconcile [options]
168
197
  run through the reconcile stage
169
198
 
170
199
  Options:
171
- --traces <path> trace input file
200
+ --traces <path> trace input file or directory
201
+ --matchers <path> declarative matcher definitions JSON file
172
202
  --include-free include zero-priced models in candidate shortlists
173
203
  --modeb-config <path> versioned Mode B runtime configuration JSON file
174
204
  -h, --help display help for command
@@ -182,7 +212,8 @@ Usage: rightmodeler scrub [options]
182
212
  run through the scrub stage
183
213
 
184
214
  Options:
185
- --traces <path> trace input file
215
+ --traces <path> trace input file or directory
216
+ --matchers <path> declarative matcher definitions JSON file
186
217
  --include-free include zero-priced models in candidate shortlists
187
218
  --modeb-config <path> versioned Mode B runtime configuration JSON file
188
219
  -h, --help display help for command
@@ -196,7 +227,8 @@ Usage: rightmodeler shortlist [options]
196
227
  run through the shortlist stage
197
228
 
198
229
  Options:
199
- --traces <path> trace input file
230
+ --traces <path> trace input file or directory
231
+ --matchers <path> declarative matcher definitions JSON file
200
232
  --include-free include zero-priced models in candidate shortlists
201
233
  --modeb-config <path> versioned Mode B runtime configuration JSON file
202
234
  -h, --help display help for command
@@ -210,7 +242,8 @@ Usage: rightmodeler replay [options]
210
242
  run through the replay stage
211
243
 
212
244
  Options:
213
- --traces <path> trace input file
245
+ --traces <path> trace input file or directory
246
+ --matchers <path> declarative matcher definitions JSON file
214
247
  --include-free include zero-priced models in candidate
215
248
  shortlists
216
249
  --modeb-config <path> versioned Mode B runtime configuration
@@ -221,6 +254,17 @@ Options:
221
254
  --max-cost-usd <amount> optional hard spend cap in USD; omit to
222
255
  run uncapped so every case and judge cell
223
256
  completes
257
+ --max-concurrency <n> maximum concurrent provider requests
258
+ --pricing-file <path> JSON map from model id to per-token input
259
+ and output USD, for catalogs without
260
+ pricing
261
+ --header <header> extra HTTP header for every provider
262
+ request, as 'name: value' (repeatable)
263
+ --catalog-reference <url-or-path> upstream /models URL or file that fills
264
+ pricing, context and capabilities the
265
+ provider catalog lacks
266
+ --policy <path> release policy JSON file: quality floor,
267
+ shortlist size, model allow and deny lists
224
268
  --evaluator <provider> external evaluator provider (choices:
225
269
  "braintrust", "langfuse", "langsmith",
226
270
  "promptfoo")
@@ -252,7 +296,8 @@ Usage: rightmodeler aggregate [options]
252
296
  run through the aggregate stage
253
297
 
254
298
  Options:
255
- --traces <path> trace input file
299
+ --traces <path> trace input file or directory
300
+ --matchers <path> declarative matcher definitions JSON file
256
301
  --include-free include zero-priced models in candidate shortlists
257
302
  --modeb-config <path> versioned Mode B runtime configuration JSON file
258
303
  -h, --help display help for command
@@ -266,7 +311,8 @@ Usage: rightmodeler confirm [options]
266
311
  run through the confirm stage
267
312
 
268
313
  Options:
269
- --traces <path> trace input file
314
+ --traces <path> trace input file or directory
315
+ --matchers <path> declarative matcher definitions JSON file
270
316
  --include-free include zero-priced models in candidate
271
317
  shortlists
272
318
  --modeb-config <path> versioned Mode B runtime configuration
@@ -277,6 +323,17 @@ Options:
277
323
  --max-cost-usd <amount> optional hard spend cap in USD; omit to
278
324
  run uncapped so every case and judge cell
279
325
  completes
326
+ --max-concurrency <n> maximum concurrent provider requests
327
+ --pricing-file <path> JSON map from model id to per-token input
328
+ and output USD, for catalogs without
329
+ pricing
330
+ --header <header> extra HTTP header for every provider
331
+ request, as 'name: value' (repeatable)
332
+ --catalog-reference <url-or-path> upstream /models URL or file that fills
333
+ pricing, context and capabilities the
334
+ provider catalog lacks
335
+ --policy <path> release policy JSON file: quality floor,
336
+ shortlist size, model allow and deny lists
280
337
  --evaluator <provider> external evaluator provider (choices:
281
338
  "braintrust", "langfuse", "langsmith",
282
339
  "promptfoo")
@@ -305,7 +362,8 @@ Usage: rightmodeler corpus [options] [command]
305
362
  build or import the replay corpus
306
363
 
307
364
  Options:
308
- --traces <path> trace input file
365
+ --traces <path> trace input file or directory
366
+ --matchers <path> declarative matcher definitions JSON file
309
367
  --include-free include zero-priced models in candidate shortlists
310
368
  --modeb-config <path> versioned Mode B runtime configuration JSON file
311
369
  -h, --help display help for command
@@ -375,7 +433,8 @@ Usage: rightmodeler audit sample [options]
375
433
  write the audit worksheet without blocking
376
434
 
377
435
  Options:
378
- --traces <path> trace input file
436
+ --traces <path> trace input file or directory
437
+ --matchers <path> declarative matcher definitions JSON file
379
438
  --include-free include zero-priced models in candidate shortlists
380
439
  --modeb-config <path> versioned Mode B runtime configuration JSON file
381
440
  -h, --help display help for command
@@ -402,9 +461,14 @@ open a draft pull request for proven model swaps
402
461
 
403
462
  Options:
404
463
  --owner <owner> GitHub repository owner
405
- --github-base-url <url> GitHub API base URL
464
+ --github-repo <repo> GitHub repository name (default: the repository
465
+ directory name)
466
+ --github-base-url <url> GitHub API base URL (default:
467
+ "https://api.github.com")
406
468
  --github-token-env <name> environment variable containing the GitHub token
407
469
  --dry-run run all machine gates without writing GitHub state
470
+ --code-graph <path> Graphify graph.json for static code context in the
471
+ pull request body; never evidence
408
472
  -h, --help display help for command
409
473
  ```
410
474
 
@@ -417,8 +481,11 @@ open a draft pull request restoring a prior model swap
417
481
 
418
482
  Options:
419
483
  --owner <owner> GitHub repository owner
484
+ --github-repo <repo> GitHub repository name (default: the repository
485
+ directory name)
420
486
  --pr <number> merged pull request number
421
- --github-base-url <url> GitHub API base URL
487
+ --github-base-url <url> GitHub API base URL (default:
488
+ "https://api.github.com")
422
489
  --github-token-env <name> environment variable containing the GitHub token
423
490
  -h, --help display help for command
424
491
  ```
@@ -474,9 +541,11 @@ reconcile one open model-swap pull request
474
541
 
475
542
  Options:
476
543
  --owner <owner> GitHub repository owner
477
- --github-repo <repo> GitHub repository name
544
+ --github-repo <repo> GitHub repository name (default: the repository
545
+ directory name)
478
546
  --pr <number> pull request number
479
- --github-base-url <url> GitHub API base URL
547
+ --github-base-url <url> GitHub API base URL (default:
548
+ "https://api.github.com")
480
549
  --github-token-env <name> environment variable containing the GitHub token
481
550
  -h, --help display help for command
482
551
  ```
@@ -489,7 +558,9 @@ Usage: rightmodeler report [options]
489
558
  write report.md and report.json
490
559
 
491
560
  Options:
492
- -h, --help display help for command
561
+ --code-graph <path> Graphify graph.json for static code context in the
562
+ report; never evidence
563
+ -h, --help display help for command
493
564
  ```
494
565
 
495
566
  ## `rightmodeler status`
@@ -503,3 +574,19 @@ Options:
503
574
  --run <runId> report one detached replay run
504
575
  -h, --help display help for command
505
576
  ```
577
+
578
+ ## `rightmodeler docs`
579
+
580
+ ```text
581
+ Usage: rightmodeler docs [options] [name]
582
+
583
+ print documentation packaged with this CLI
584
+
585
+ Arguments:
586
+ name packaged document name (choices: "commands", "evaluators",
587
+ "exit-codes", "gateways", "getting-started", "github",
588
+ "github-actions", "modeb")
589
+
590
+ Options:
591
+ -h, --help display help for command
592
+ ```