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.
- package/dist-bundle/cli.js +9432 -5081
- package/dist-bundle/provenance.js +89 -0
- package/dist-bundle/proxy/container-supervisor.mjs +6 -0
- package/dist-bundle/proxy/proxy-runtime.mjs +105 -13
- package/dist-bundle/transport/stream.js +8 -2
- package/docs/commands.md +105 -18
- package/docs/evaluators.md +88 -9
- package/docs/exit-codes.md +41 -1
- package/docs/gateways.md +109 -0
- package/docs/getting-started.md +131 -6
- package/docs/github-actions.md +328 -0
- package/docs/github.md +110 -0
- package/docs/modeb.md +34 -0
- package/package.json +4 -1
package/docs/evaluators.md
CHANGED
|
@@ -1,16 +1,95 @@
|
|
|
1
1
|
# Evaluators
|
|
2
2
|
|
|
3
|
-
The default evaluator is the built-in judge selected from the configured provider catalog. Candidate and reference families are excluded when choosing the judge.
|
|
3
|
+
The default evaluator is the built-in judge selected from the configured provider catalog. Candidate and reference families are excluded when choosing the judge. The exclusion applies per replayed call site: when the call sites of one traced family use models of different vendors (for example `openai/...` and `anthropic/...`), each call site's cases are judged by a model from neither the candidate's model family nor that call site's. Confirmation judges the recorded final output, so its judge also comes from outside the final step's model family.
|
|
4
4
|
|
|
5
|
-
An external evaluator
|
|
5
|
+
An external evaluator is requested with `--evaluator <provider>` on `init`, `estimate`, `replay`, and `confirm`, where the provider is `braintrust`, `langfuse`, `langsmith`, or `promptfoo`. Every other `--evaluator-*` option is a usage error without `--evaluator`.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- one or more `--evaluator-scorer <name>` options (required)
|
|
9
|
-
- `--evaluator-api-key-env <name>` (defaults to `BRAINTRUST_API_KEY`)
|
|
10
|
-
- `--evaluator-base-url <url>` (defaults to the provider API)
|
|
11
|
-
- `--evaluator-gate-metric <name>` when more than one scorer is configured
|
|
12
|
-
- `--evaluator-gate-threshold <value>` when the evaluator does not return a pass decision
|
|
7
|
+
## Options shared by every provider
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
- `--evaluator-scorer <name>` (required, repeatable): the scorer names the provider must return for every case.
|
|
10
|
+
- `--evaluator-gate-metric <name>`: the scorer used for release gates. Required when more than one scorer is configured; with a single scorer it defaults to that scorer. It must name a configured scorer.
|
|
11
|
+
- `--evaluator-gate-threshold <value>`: pass threshold applied only when the evaluator omits a pass decision for a metric.
|
|
12
|
+
|
|
13
|
+
## Provider options
|
|
14
|
+
|
|
15
|
+
One table per provider. A rejected option is a usage error with that provider (exit code 2, `invalid_option`).
|
|
16
|
+
|
|
17
|
+
### braintrust
|
|
18
|
+
|
|
19
|
+
`--evaluator-project-id` names the Braintrust project that receives the experiment.
|
|
20
|
+
|
|
21
|
+
| Option | Requirement | Default |
|
|
22
|
+
| ----------------------------------- | ----------- | ---------------------------- |
|
|
23
|
+
| `--evaluator-project-id <id>` | required | |
|
|
24
|
+
| `--evaluator-api-key-env <name>` | optional | `BRAINTRUST_API_KEY` |
|
|
25
|
+
| `--evaluator-base-url <url>` | optional | `https://api.braintrust.dev` |
|
|
26
|
+
| `--evaluator-public-key-env <name>` | rejected | |
|
|
27
|
+
| `--evaluator-command <path>` | rejected | |
|
|
28
|
+
| `--evaluator-config <path>` | rejected | |
|
|
29
|
+
|
|
30
|
+
### langfuse
|
|
31
|
+
|
|
32
|
+
Langfuse authenticates with a public key and a secret key read from environment variables. The keys select the project, so there is no project option.
|
|
33
|
+
|
|
34
|
+
| Option | Requirement | Default |
|
|
35
|
+
| ----------------------------------- | ----------- | ---------------------------- |
|
|
36
|
+
| `--evaluator-api-key-env <name>` | optional | `LANGFUSE_SECRET_KEY` |
|
|
37
|
+
| `--evaluator-public-key-env <name>` | optional | `LANGFUSE_PUBLIC_KEY` |
|
|
38
|
+
| `--evaluator-base-url <url>` | optional | `https://cloud.langfuse.com` |
|
|
39
|
+
| `--evaluator-project-id <id>` | rejected | |
|
|
40
|
+
| `--evaluator-command <path>` | rejected | |
|
|
41
|
+
| `--evaluator-config <path>` | rejected | |
|
|
42
|
+
|
|
43
|
+
### langsmith
|
|
44
|
+
|
|
45
|
+
`--evaluator-project-id` names the LangSmith dataset the experiment references. A scorer may be written `metric=rule-id` to bind a metric name to an evaluator rule; a bare name is used for both.
|
|
46
|
+
|
|
47
|
+
| Option | Requirement | Default |
|
|
48
|
+
| ----------------------------------- | ----------- | --------------------------------- |
|
|
49
|
+
| `--evaluator-project-id <id>` | required | |
|
|
50
|
+
| `--evaluator-api-key-env <name>` | optional | `LANGSMITH_API_KEY` |
|
|
51
|
+
| `--evaluator-base-url <url>` | optional | `https://api.smith.langchain.com` |
|
|
52
|
+
| `--evaluator-public-key-env <name>` | rejected | |
|
|
53
|
+
| `--evaluator-command <path>` | rejected | |
|
|
54
|
+
| `--evaluator-config <path>` | rejected | |
|
|
55
|
+
|
|
56
|
+
### promptfoo
|
|
57
|
+
|
|
58
|
+
promptfoo runs locally. `--evaluator-config` is the assertions file passed to `promptfoo eval --assertions`; `--evaluator-command` is the executable, one path or one command on `PATH` (so not `npx promptfoo`: install it with `npm install -g promptfoo@0.123.1` or pass the path to its binary). No API or project option applies.
|
|
59
|
+
|
|
60
|
+
| Option | Requirement | Default |
|
|
61
|
+
| ----------------------------------- | ----------- | ----------- |
|
|
62
|
+
| `--evaluator-config <path>` | required | |
|
|
63
|
+
| `--evaluator-command <path>` | optional | `promptfoo` |
|
|
64
|
+
| `--evaluator-base-url <url>` | rejected | |
|
|
65
|
+
| `--evaluator-api-key-env <name>` | rejected | |
|
|
66
|
+
| `--evaluator-project-id <id>` | rejected | |
|
|
67
|
+
| `--evaluator-public-key-env <name>` | rejected | |
|
|
68
|
+
|
|
69
|
+
rightmodeler is verified against promptfoo 0.123.1. Other releases work as long as they write the same results layout; one that does not stops the run with an error naming its version and the layout rightmodeler reads. A missing executable counts as unreachable (see below); any other promptfoo failure, such as a broken `promptfooconfig.*` next to the assertions file, stops the run with promptfoo's own message.
|
|
70
|
+
|
|
71
|
+
Each evaluation runs one command in the assertions file's directory, with standard input closed:
|
|
72
|
+
|
|
73
|
+
`<command> eval --assertions <assertions file> --model-outputs <model outputs file, relative to that directory> --output <temporary results file> --no-write --no-share --no-table --no-progress-bar`
|
|
74
|
+
|
|
75
|
+
The command gets your environment plus `PROMPTFOO_DISABLE_UPDATE=true`, `PROMPTFOO_DISABLE_VAR_EXPANSION=true`, `PROMPTFOO_FAILED_TEST_EXIT_CODE=100`, `PROMPTFOO_SHORT_CIRCUIT_TEST_FAILURES=false`, `PROMPTFOO_STRIP_GRADING_RESULT=false`, `PROMPTFOO_STRIP_RESPONSE_OUTPUT=false`, and `PROMPTFOO_STRIP_TEST_VARS=false`. So every output is graded exactly once against every assertion, the batch is neither saved to promptfoo's history nor shared, promptfoo skips its startup update check, and your own promptfoo settings cannot change the exit code, turn a failed assertion into an error, or strip the fields rightmodeler reads. Everything else, including your promptfoo login, cache, telemetry setting, and model-graded assertion providers, works as when you run promptfoo yourself. promptfoo receives each candidate output and its execution id only; the input, messages, and reference answer are never sent.
|
|
76
|
+
|
|
77
|
+
Put `metric: <name>` on every assertion that feeds an `--evaluator-scorer`. A metric's score is promptfoo's named score for it, and it passes only when every assertion carrying it passes. promptfoo always decides pass or fail, so `--evaluator-gate-threshold` never applies. Each assessment's rubric version is `promptfoo@<version>/<metric>/<digest>`, where the digest is the first 16 hex characters of the canonical SHA-256 of the assertions carrying that metric, so editing one of them, or upgrading promptfoo, changes it. A scorer that no assertion carries stops the run with an error naming it.
|
|
78
|
+
|
|
79
|
+
Before grading, promptfoo renders `{{ }}` templates in an output, reads a `file://` path or loads a `package:` module it names, and strips one trailing newline. rightmodeler compares the output promptfoo graded with the output it sent. A case whose graded output differs beyond that one newline is recorded absent as `external_output_mismatch`, because the grade is not of the candidate's output; a case promptfoo could not grade at all, such as a `file://` path that does not exist, is recorded absent as `external_evaluator_error`, and the other cases in its batch keep their grades. Neither counts as evidence. Nor does a metric whose grader failed, which promptfoo marks `graderError` (for example when a model-graded assertion's grading call fails): it is left out rather than recorded as a failure, and a case that loses its gate metric this way is recorded absent as `external_evaluator_error`. Replayed outputs are kept in the store, so a rerun after fixing promptfoo does not repeat model calls.
|
|
80
|
+
|
|
81
|
+
promptfoo also loads a `promptfooconfig.*` and a `.env` file from the directory it runs in, so any next to the assertions file apply: a `defaultTest` there adds its assertions to every case, a `defaultTest` transform makes every case `external_output_mismatch`, and its `env` block can override the settings above. Every grade, and a detached replay's identity (`replay --detach`), covers the assertions file and every `promptfooconfig.*` next to it (see [Changing an evaluator](#changing-an-evaluator)); a `.env` there is not covered. Keep the assertions file in a directory of its own unless you want those files to apply.
|
|
82
|
+
|
|
83
|
+
## Changing an evaluator
|
|
84
|
+
|
|
85
|
+
Every grade records the evaluator configuration it was made under: the provider, every evaluator option as given, and for promptfoo the bytes of the assertions file and of every `promptfooconfig.*` beside it. When that configuration changes, the next `init` or `replay` re-grades every stored candidate output that was graded under another configuration. The outputs are reused rather than replayed, so no candidate model is called again, and an `evaluator_regrade` warning counts the outputs being re-graded and says why. The earlier grades stay in the store, but no verdict or report uses them, and `export` leaves them out. A replay run after upgrading from a version that did not record the configuration re-grades once for the same reason.
|
|
86
|
+
|
|
87
|
+
`--evaluator-scorer`, `--evaluator-gate-metric` and `--evaluator-gate-threshold` are different: they define the question the evidence answers, so changing one replays the candidates again. Whenever replay runs, a model the provider catalog now selects that was never replayed is replayed like any new candidate, within `--max-cost-usd`.
|
|
88
|
+
|
|
89
|
+
rightmodeler cannot see a change it is not given: a scorer edited inside Braintrust, an evaluator edited inside Langfuse, a LangSmith rule edited in place, a file an assertion loads with `file://`, a `.env` beside the assertions file, or a promptfoo upgrade behind the same `--evaluator-command`. To re-grade after one of these, edit the assertions file, or bind the LangSmith metric to the new rule's id (`metric=rule-id`). For Braintrust and Langfuse, the grades already held stay in use until an evaluator option changes.
|
|
90
|
+
|
|
91
|
+
## Reachability, polling, and absences
|
|
92
|
+
|
|
93
|
+
If the external evaluator is unreachable, the pipeline warns and uses the built-in judge. Hosted scorers run asynchronously, so after launching a run the pipeline polls for up to 5 minutes, starting 250 ms after launch and doubling the wait between polls up to 10 seconds. If a reachable run fails, is still pending when the budget ends, or omits required case results, the pipeline records the missing assessments instead of fabricating scores.
|
|
15
94
|
|
|
16
95
|
See [Commands](commands.md) for the complete option text and [Getting started](getting-started.md) for provider setup.
|
package/docs/exit-codes.md
CHANGED
|
@@ -14,13 +14,53 @@ Rightmodeler reserves exit codes `0` through `3` for machine-readable outcomes.
|
|
|
14
14
|
|
|
15
15
|
- `0`: changes were applied, or a dry run was clean.
|
|
16
16
|
- `1`: the change was refused by a machine gate.
|
|
17
|
+
- `2`: no completed run in the store (for example `stage_not_completed`); the JSON error on standard error names the remedy.
|
|
18
|
+
- `10` or greater: runtime failure.
|
|
19
|
+
|
|
20
|
+
See [GitHub](github.md) for tokens, reviewers and refusal codes.
|
|
21
|
+
|
|
22
|
+
## Rollback
|
|
23
|
+
|
|
24
|
+
- `0`: the rollback pull request was opened, or already exists.
|
|
25
|
+
- `1`: the rollback was refused by a machine gate.
|
|
26
|
+
- `10` or greater: runtime failure.
|
|
27
|
+
|
|
28
|
+
## Drift
|
|
29
|
+
|
|
30
|
+
- `0`: the drift check completed.
|
|
31
|
+
- `2`: `--traces` is missing.
|
|
17
32
|
- `10` or greater: runtime failure.
|
|
18
33
|
|
|
19
34
|
## Watch
|
|
20
35
|
|
|
21
36
|
- `0`: no action was required.
|
|
22
37
|
- `1`: review or continuous-integration actions were taken.
|
|
23
|
-
- `2`: another watcher holds the lock.
|
|
38
|
+
- `2`: another watcher holds the lock, or the store has no completed run. A held lock prints a result with `"status":"lock_held"` on standard output; a missing run prints an error such as `stage_not_completed` on standard error and nothing on standard output.
|
|
24
39
|
- `10` or greater: runtime failure.
|
|
25
40
|
|
|
26
41
|
Use `--output json` for one result object or `--output jsonl` for stage events followed by the result. Errors use the selected machine-readable mode on standard error. See [Commands](commands.md) for command-specific options.
|
|
42
|
+
|
|
43
|
+
## Error codes
|
|
44
|
+
|
|
45
|
+
- `active_corpus_usage_unavailable` (exit `2`): publish a corpus version built from traces that include token usage, then rerun.
|
|
46
|
+
- `ambiguous_trace_format` (exit `2`): pass a trace file that unambiguously matches one supported format.
|
|
47
|
+
- `budget_cap_refusal` (exit `3`): the run reached the cost boundary; raise `--max-cost-usd` to the cap named in the remedy and rerun.
|
|
48
|
+
- `coverage_gate_failed` (exit `2`): add matcher coverage for the listed AI dependency surfaces, or pass `--matchers <file>` with declarative matchers that close them.
|
|
49
|
+
- `empty_traces_directory` (exit `2`): point `--traces` at a directory containing `.json` or `.jsonl` trace files, or at a single trace file.
|
|
50
|
+
- `git_repository_has_no_commits` (exit `2`): create the first commit, then rerun the command.
|
|
51
|
+
- `invalid_catalog_reference` (exit `2`): pass `--catalog-reference` an http(s) URL or a readable file that returns an OpenAI-compatible `/models` document, or remove it, then rerun.
|
|
52
|
+
- `invalid_matchers_file` (exit `2`): fix the listed matcher definitions in the `--matchers` file and rerun.
|
|
53
|
+
- `invalid_modeb_config` (exit `2`): fix the named field in the `--modeb-config` file and rerun.
|
|
54
|
+
- `invalid_option` (exit `2`): correct the option and rerun; use `rightmodeler <command> --help` for accepted values.
|
|
55
|
+
- `invalid_policy_file` (exit `2`): fix the named field in the `--policy` file and rerun; `qualityFloor` must be greater than 0.8 and less than 1, `shortlistTop` a positive integer, `allowModels` and `denyModels` arrays of model ids.
|
|
56
|
+
- `invalid_pricing_file` (exit `2`): fix `--pricing-file` to map each model id to non-negative `input` and `output` USD per token and, optionally, a positive integer `maxOutputTokens`, then rerun.
|
|
57
|
+
- `missing_provider_configuration` (exit `2`): pass `--base-url <url>` and, if needed, `--api-key-env <environment-variable-name>` naming a populated variable.
|
|
58
|
+
- `missing_traces_path` (exit `2`): pass `--traces <path>` pointing to an existing trace file or directory.
|
|
59
|
+
- `mixed_trace_formats` (exit `2`): split the directory so every file uses the same trace format, or pass one file with `--traces`.
|
|
60
|
+
- `modeb_cloud_unavailable` (exit `2`): install the optional sandbox SDK and set its credentials, or set `"backend": "docker"` in the `--modeb-config` file, then rerun.
|
|
61
|
+
- `no_priced_candidates` (exit `2`): point `--base-url` at a catalog that publishes per-token pricing, pass `--catalog-reference <url>`, expose priced LiteLLM `GET /model/info`, or pass `--pricing-file <path>`, then rerun.
|
|
62
|
+
- `no_replayable_call_sites` (exit `2`): point `--repo` at a service with plain text completions, or add a matcher for a text call site, then rerun.
|
|
63
|
+
- `not_git_repository` (exit `2`): run the command again from a Git repository with at least one commit.
|
|
64
|
+
- `stage_not_completed` (exit `2`): run `rightmodeler init --through <stage>` first, then rerun the command.
|
|
65
|
+
- `unusable_trace_input` (exit `2`): the selected discovered trace could not be adapted; rerun and choose a different trace file.
|
|
66
|
+
- `usage_error` (exit `10`): the command line is invalid; `message` carries the parser text.
|
package/docs/gateways.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Gateways
|
|
2
|
+
|
|
3
|
+
Rightmodeler replays and judges through any OpenAI-compatible base URL, so a gateway in front of your models can be the replay route. This guide covers open-source gateways verified at pinned releases.
|
|
4
|
+
|
|
5
|
+
## What a replay route needs
|
|
6
|
+
|
|
7
|
+
- `--base-url` is the gateway's OpenAI-compatible root, ending in `/v1`, and `--api-key-env` names the variable holding the key the gateway expects.
|
|
8
|
+
- The gateway's `/v1/models` lists the replay models. When it lists ids without prices or capabilities, pass `--catalog-reference` with the upstream's public model list; see [Catalogs without pricing or capabilities](getting-started.md#catalogs-without-pricing-or-capabilities).
|
|
9
|
+
- Headers the gateway reads per request are passed with `--header 'name: value'`; see [Gateways that route by header](getting-started.md#gateways-that-route-by-header).
|
|
10
|
+
- The replay route runs no fallbacks, model aliases, response caching or request plugins for the replay models. Rightmodeler never asks a gateway for them, and it checks each replayed response (in Mode B, each response to a step whose model it sets): one that names another model, reports a cache hit, or reports a changed request is left out of the evidence as `attribution_substituted`; see [Which model answered](getting-started.md#which-model-answered).
|
|
11
|
+
- Replay latency includes the gateway hop; rightmodeler does not separate the gateway's share.
|
|
12
|
+
- Mode B on the cloud backend calls the base URL from a remote sandbox, so it cannot reach a gateway on localhost or a private network. Use the Docker backend for a local gateway.
|
|
13
|
+
|
|
14
|
+
## Portkey
|
|
15
|
+
|
|
16
|
+
Verified on the open-source Portkey gateway 1.15.2 (MIT, `portkeyai/gateway:1.15.2`), the latest tagged release as of 2026-09-22. Portkey picks the upstream for each request from two headers and needs no server configuration:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
docker run -d --name portkey -p 127.0.0.1:8787:8787 portkeyai/gateway:1.15.2
|
|
20
|
+
npx rightmodeler init --traces ./traces.jsonl --repo . \
|
|
21
|
+
--base-url http://127.0.0.1:8787/v1 --api-key-env AI_GATEWAY_API_KEY \
|
|
22
|
+
--header 'x-portkey-provider: openai' \
|
|
23
|
+
--header 'x-portkey-custom-host: https://ai-gateway.vercel.sh/v1' \
|
|
24
|
+
--max-cost-usd 25
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Use the `openai` provider with a custom host for any OpenAI-compatible upstream (Vercel AI Gateway, OpenRouter, a LiteLLM proxy). Portkey's `openrouter` provider rewrites requests and does not serve the model catalog.
|
|
28
|
+
- `--api-key-env` holds the upstream's key: Portkey forwards `Authorization` to the upstream and has no key of its own.
|
|
29
|
+
- The catalog, per-call cost, and rate-limit and credit errors come from the upstream through Portkey unchanged.
|
|
30
|
+
- Rightmodeler sends no `x-portkey-config`, so fallbacks, load balancing, retries, caching and guardrail or mutator hooks stay off. If you pass one with `--header`, a replayed response whose model differs (`override_params`, targets), that reports `x-portkey-cache-status: HIT`, or whose hook results show `transformed: true` is left out as `attribution_substituted`.
|
|
31
|
+
- Bind the container to 127.0.0.1, or start it headless (`docker run ... portkeyai/gateway:1.15.2 run start:node -- --headless`): the 1.15.2 console and live log stream are served without authentication and show provider keys.
|
|
32
|
+
- Portkey 1.15.2 keeps no traces or logs that rightmodeler can read. Export traces from your application: OpenTelemetry GenAI, AI SDK telemetry, or the OpenAI SDK JSONL shape.
|
|
33
|
+
|
|
34
|
+
## Envoy AI Gateway (Agent Router)
|
|
35
|
+
|
|
36
|
+
Envoy AI Gateway was renamed Agent Router on 2026-09-09; its resources, `aigw` CLI and images keep their names. Verified on v1.1.0 (Apache 2.0) running standalone with `aigw run` (`envoyproxy/ai-gateway-cli:v1.1.0`); always use a release tag, because the `latest` image follows the development branch.
|
|
37
|
+
|
|
38
|
+
As a replay route:
|
|
39
|
+
|
|
40
|
+
- Declare each replay model as an `Exact` `x-ai-eg-model` match under the upstream's own id, one `backendRef`, no `modelNameOverride`, no priority fallback and no `BackendTrafficPolicy` retries on the replay route. A fallback or an override answers with another model, so a replayed response it answers is left out as `attribution_substituted`.
|
|
41
|
+
- Raise the Gateway's `ClientTrafficPolicy` `bufferLimit` (Envoy Gateway's 32 KiB default is too small for real prompts) and the route's `timeouts.request` for slow models.
|
|
42
|
+
- The gateway lists only the declared ids at `/v1/models`, so pass `--catalog-reference` with the upstream's public list.
|
|
43
|
+
- The gateway replaces `Authorization` with the route's key, so `--api-key-env` may name any non-empty variable.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
docker run --rm -p 127.0.0.1:1975:1975 -e AI_GATEWAY_API_KEY \
|
|
47
|
+
-e OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 \
|
|
48
|
+
-e 'OTEL_AIGW_SPAN_REQUEST_HEADER_ATTRIBUTES=agent-session-id:session.id,x-rightmodeler-family:rightmodeler.family,x-rightmodeler-replay:rightmodeler.replay' \
|
|
49
|
+
-v "$PWD/aigw.yaml:/config.yaml:ro" envoyproxy/ai-gateway-cli:v1.1.0 run /config.yaml
|
|
50
|
+
export AIGW_CLIENT_KEY=unused
|
|
51
|
+
npx rightmodeler init --traces ./spans.jsonl --repo . \
|
|
52
|
+
--base-url http://127.0.0.1:1975/v1 --api-key-env AIGW_CLIENT_KEY \
|
|
53
|
+
--catalog-reference https://ai-gateway.vercel.sh/v1/models \
|
|
54
|
+
--header 'x-rightmodeler-replay: 1' --max-cost-usd 25
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
As a trace source, rightmodeler reads the gateway's default OpenInference spans from an OpenTelemetry collector's file export:
|
|
58
|
+
|
|
59
|
+
- Set `OTEL_EXPORTER_OTLP_ENDPOINT`. Map headers to span attributes with `OTEL_AIGW_SPAN_REQUEST_HEADER_ATTRIBUTES` as comma-separated `header:attribute` pairs; setting it replaces the default `agent-session-id:session.id`, so repeat that pair.
|
|
60
|
+
- Send `agent-session-id` from your application so the steps of one conversation form one ordered run, and `x-rightmodeler-family: <name>` so each call has its family. Send `x-rightmodeler-replay: 1` with rightmodeler's replays (`--header`) so a later export leaves them out.
|
|
61
|
+
- Each span's request body gives the model your application asked for and the conversation as sent; the response gives the output; the token counts give usage.
|
|
62
|
+
- Failed calls, replay-tagged calls, and calls whose prompt or output the gateway hid (`OPENINFERENCE_HIDE_INPUTS`, `OPENINFERENCE_HIDE_OUTPUTS`) are left out of the corpus with a `trace_steps_excluded` warning that names each reason.
|
|
63
|
+
- A span records the model that answered but not whether a priority fallback chose it (a `modelNameOverride` alias looks the same), so a call that fell back is read as an answer to the model your application asked for, with the fallback's output and usage. Keep fallback routes off the traffic you export, or expect those answers among the recorded outputs rightmodeler compares candidates against.
|
|
64
|
+
- Steps whose recorded conversation contains tool calls are read but not replayed yet (`recorded_messages_not_replayable`).
|
|
65
|
+
- With `AI_GATEWAY_TRACING_SEMCONV=gen_ai`, the spans are read by the OTel GenAI reader instead. Also set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`, because that convention records no messages without it and rightmodeler cannot read a span with none. Those spans are grouped by trace (propagate `traceparent` from your client), carry no `response_format` or `tool_choice`, and record images by type only.
|
|
66
|
+
- Access logs carry no message content and are not a trace source.
|
|
67
|
+
|
|
68
|
+
On Kubernetes (Kubernetes 1.32 or newer, Envoy Gateway 1.8.1 or newer, Helm charts `ai-gateway-crds-helm` and `ai-gateway-helm` v1.1.0), the same resources apply; set `OTEL_EXPORTER_OTLP_ENDPOINT` through the `ai-gateway-helm` chart's `extProc.extraEnvVars` and the header mapping through its `controller.spanRequestHeaderAttributes`. Mode B on the cloud backend cannot reach an in-cluster gateway.
|
|
69
|
+
|
|
70
|
+
## Bifrost
|
|
71
|
+
|
|
72
|
+
Verified on the open-source Bifrost gateway transports/v2.2.1 (Apache 2.0, `maximhq/bifrost:v2.2.1`); pin the image, because releases arrive weekly. Enterprise features are a separate image and are not covered here.
|
|
73
|
+
|
|
74
|
+
As a replay route:
|
|
75
|
+
|
|
76
|
+
- Model ids are `<provider>/<upstream id>`, for example `vercel/openai/gpt-4o-mini` for Vercel AI Gateway configured as an OpenAI-typed custom provider. Bifrost answers with the upstream id, which rightmodeler accepts as the requested model.
|
|
77
|
+
- Set every `compat` flag to `false` in the `client` block: a `client` block that omits them turns them all on, and the compat plugin can drop parameters such as `response_format` while answering 200.
|
|
78
|
+
- Configure no key `aliases` or routing rules for replay models; an alias answers with another model and is left out as `attribution_substituted`.
|
|
79
|
+
- Pass `--catalog-reference` with the upstream's public list. A custom provider lists ids and context only, with no prices or capabilities, and through Vercel AI Gateway every model carries the same `created` date: Bifrost copies Vercel's `created`, one placeholder for all models, and drops the real `released` date. Rightmodeler ranks judges partly by release date, and the reference's release dates win over the gateway's, so judges rank as they do on Vercel itself instead of the most expensive first. Bifrost's own pricing sheet is not used.
|
|
80
|
+
- The billed cost comes from the upstream through Bifrost's `usage.cost.total_cost`.
|
|
81
|
+
- Send `x-bf-cache-no-store: true` and `x-bf-dim-rightmodeler: replay` with rightmodeler's replays (`--header`): the first keeps replays out of a semantic cache (a cache hit is still detected and left out), the second tags them so a later log export leaves them out.
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
docker volume create bifrost-data
|
|
85
|
+
docker create --name bifrost -p 127.0.0.1:8080:8080 -e AI_GATEWAY_API_KEY -v bifrost-data:/app/data maximhq/bifrost:v2.2.1
|
|
86
|
+
docker cp config.json bifrost:/app/data/config.json && docker start bifrost
|
|
87
|
+
export BIFROST_KEY=unused
|
|
88
|
+
npx rightmodeler init --traces ./bifrost-logs.jsonl --repo . \
|
|
89
|
+
--base-url http://127.0.0.1:8080/v1 --api-key-env BIFROST_KEY \
|
|
90
|
+
--catalog-reference https://ai-gateway.vercel.sh/v1/models \
|
|
91
|
+
--header 'x-bf-cache-no-store: true' --header 'x-bf-dim-rightmodeler: replay' \
|
|
92
|
+
--max-cost-usd 25
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
As a trace source, rightmodeler reads Bifrost's own request logs, exported from its management API (add credentials once an admin exists):
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
curl -s 'http://127.0.0.1:8080/api/logs?objects=chat_completion,chat_completion_stream&order=asc&limit=1000&offset=0' | jq -r '.logs[].id' \
|
|
99
|
+
| while read -r id; do curl -s "http://127.0.0.1:8080/api/logs/$id"; echo; done > bifrost-logs.jsonl
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
With more than 1000 rows, repeat with `offset=1000`, `2000` and so on, appending (`>>`) to the same file, until a page returns fewer than 1000 rows; `order=asc` keeps earlier pages in place while new calls are logged. Do not pass `roots_only=true`: it hides fallback rows.
|
|
103
|
+
|
|
104
|
+
- Send `x-bf-session-id` from your application so the steps of one conversation form one ordered run, and `x-bf-dim-rightmodeler-family: <name>` so each call has its family.
|
|
105
|
+
- Each row gives the model the application asked for (`provider/model`, or the alias it sent), the conversation as sent (`input_history`), the output (`output_message`), usage, cost, latency and retries.
|
|
106
|
+
- Failed calls, answers from a configured fallback, rightmodeler's own replays and calls whose content was not logged are left out of the corpus with a `trace_steps_excluded` warning that names each reason. Steps whose conversation contains tool calls are read but not replayed yet.
|
|
107
|
+
- Bifrost's OpenTelemetry plugin export reads through the OTel GenAI reader, but it summarizes messages and loses tool-call ids: use the log export for anything but plain text calls.
|
|
108
|
+
- Streamed calls through Mode B are checked on the stream's model and headers only; Bifrost reports a semantic-cache hit only in the response body, so such a hit on a streamed Mode B call is not detected.
|
|
109
|
+
- Rightmodeler records every replay's latency through Bifrost, gateway hop included, and does not separate Bifrost's own overhead. Bifrost's published benchmark reports 11 microseconds of overhead on a t3.xlarge at 5,000 requests per second, excluding JSON marshalling and the HTTP call; measure it on your own traffic.
|
package/docs/getting-started.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# Getting started
|
|
2
2
|
|
|
3
|
-
Rightmodeler analyzes recorded model calls, replays them against cheaper candidates, evaluates the outputs, and writes a recommendation report.
|
|
3
|
+
Rightmodeler analyzes recorded model calls, replays them against cheaper candidates, evaluates the outputs, and writes a recommendation report. Published on npm as `rightmodeler`.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js 24 or newer.
|
|
8
8
|
- A Git repository to analyze.
|
|
9
9
|
- Trace input in a supported format.
|
|
10
|
-
- An OpenAI-compatible provider base URL and the name of an environment variable containing its API key before replay begins.
|
|
10
|
+
- An OpenAI-compatible provider base URL and the name of an environment variable containing its API key before replay begins. Its `/v1/models` catalog should publish per-token pricing. OpenRouter and Vercel AI Gateway do. For a LiteLLM endpoint, Rightmodeler can fall back to `GET /model/info`; for a gateway that lists bare model ids, pass `--catalog-reference`; for bare OpenAI or another unpriced endpoint, pass `--pricing-file`.
|
|
11
11
|
|
|
12
|
-
Supported trace sources are OTel GenAI,
|
|
13
|
-
LangSmith, OpenInference, Helicone, W&B Weave,
|
|
12
|
+
Supported trace sources are OTel GenAI, AI SDK telemetry, OpenAI JSONL,
|
|
13
|
+
Langfuse, Braintrust, LangSmith, OpenInference, Helicone, Bifrost, W&B Weave,
|
|
14
|
+
Claude Code, and Codex.
|
|
14
15
|
|
|
15
16
|
## Start with automatic discovery
|
|
16
17
|
|
|
@@ -40,6 +41,27 @@ npx rightmodeler init --plan --output json --repo /path/to/repository
|
|
|
40
41
|
npx rightmodeler init --through corpus --traces /path/to/traces.json --output json --repo /path/to/repository
|
|
41
42
|
```
|
|
42
43
|
|
|
44
|
+
`--traces` accepts a single file or a directory. A directory is read non-recursively as its `.json` and `.jsonl` files in name order; every file must use the same trace format.
|
|
45
|
+
|
|
46
|
+
Each family is replayed only against the call sites its own traces came from. Traced cases that cannot be tied to one such call site, because several call sites use the traced model or none matches it, are left out of the replay sample with a `family_cases_left_out` warning. A family with no case left abstains before any spend with `ambiguous_call_site_binding` or `unmatched_call_site_binding`. The AI SDK telemetry `functionId` (see below) is the way to tie an AI SDK call site to its family.
|
|
47
|
+
|
|
48
|
+
## AI SDK telemetry
|
|
49
|
+
|
|
50
|
+
The AI SDK emits telemetry in two dialects, and Rightmodeler reads both:
|
|
51
|
+
|
|
52
|
+
- The `ai.*` dialect comes from AI SDK 5 and 6 with `experimental_telemetry: { isEnabled: true }` on each call, and from AI SDK 7 with `registerTelemetry(new LegacyOpenTelemetry())`. The AI SDK reader reads it.
|
|
53
|
+
- The GenAI semantic conventions dialect comes from AI SDK 7 with `registerTelemetry(new OpenTelemetry())`. The OTel GenAI reader reads it and treats the agent, step and tool spans as structure, so each model call counts once.
|
|
54
|
+
|
|
55
|
+
`registerTelemetry` comes from `ai`; `LegacyOpenTelemetry` and `OpenTelemetry` come from `@ai-sdk/otel`. Register only one of them: an export that holds both dialects is ambiguous. Keep `recordInputs` and `recordOutputs` on, which is the default, because a model call without its prompt or output cannot become a corpus case. Set a string-literal `functionId` on every call (`telemetry: { functionId: "summarize" }` in AI SDK 7, `experimental_telemetry: { isEnabled: true, functionId: "summarize" }` before it); it becomes the call's family.
|
|
56
|
+
|
|
57
|
+
The scanner records that `functionId` on the call site, and a family binds to exactly the call sites whose `functionId` equals its name: its evidence and any swap stay on those call sites, and no other family borrows them. Two call sites that do the same job may share one `functionId`. A family bound to a single call site can still be recommended. The `functionId` must be a string literal in the call; a variable or a template literal is not read, and the call site then binds by model id only. Replay cannot run a call site that needs tools or structured output: its cases are left out of the family's replay sample with a `family_cases_left_out` warning, and a family whose traced cases all come from such call sites abstains with `bound_call_sites_not_replayable` before any spend.
|
|
58
|
+
|
|
59
|
+
Export the spans through the OpenTelemetry NodeSDK or `@vercel/otel` to an OTLP collector, and pass the collector's file exporter output with `--traces`. A model call that ended without a finish reason, because it was aborted or errored, is left out of the corpus with a `trace_steps_excluded` warning, and the rest of the input is read. Token usage from AI SDK 4 exports (`ai.usage.promptTokens`) is not read, so those calls carry no usage.
|
|
60
|
+
|
|
61
|
+
## OpenInference spans
|
|
62
|
+
|
|
63
|
+
When an OpenInference span carries the request body in `input.value` (Envoy AI Gateway, and the OpenAI instrumentation), rightmodeler reads the model your application asked for and the conversation exactly as sent from it, and the output from `output.value`. Spans that share a `session.id` form one run ordered by start time. A `rightmodeler.family` attribute names the family; otherwise the span name does. Failed calls, calls tagged `rightmodeler.replay`, and calls whose content was hidden are left out with a `trace_steps_excluded` warning.
|
|
64
|
+
|
|
43
65
|
## Run the complete pipeline
|
|
44
66
|
|
|
45
67
|
```sh
|
|
@@ -51,6 +73,8 @@ npx rightmodeler init --traces /path/to/traces.json --base-url https://provider.
|
|
|
51
73
|
`--api-key-env <name>` to use a different exported variable. The CLI does not ask
|
|
52
74
|
for a secret value.
|
|
53
75
|
|
|
76
|
+
Replay resends each recorded conversation as text. A recorded case whose conversation contains tool calls, non-text parts, or tool definitions is left out of the replay sample with a `recorded_messages_not_replayable` warning; the family's other cases replay, and a family left with too few cases abstains under the usual sample-size reasons.
|
|
77
|
+
|
|
54
78
|
## Estimate replay spend
|
|
55
79
|
|
|
56
80
|
```sh
|
|
@@ -59,7 +83,108 @@ npx rightmodeler estimate --traces /path/to/traces.json --base-url https://provi
|
|
|
59
83
|
|
|
60
84
|
Estimate projects candidate replay spend from recorded token usage and the current
|
|
61
85
|
model catalog before paid model calls begin.
|
|
86
|
+
`--max-cost-usd` caps candidate replays and judge calls together: each call reserves
|
|
87
|
+
its worst case before it is sent, and a call the cap cannot cover is not sent.
|
|
88
|
+
|
|
89
|
+
## Static code context (Graphify)
|
|
90
|
+
|
|
91
|
+
rightmodeler can read a code graph built by the open-source Graphify CLI (PyPI package `graphifyy`, Apache-2.0, tested with 0.9.65). Graphify builds it locally from your source, with no account and no model call.
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
uv tool install graphifyy
|
|
95
|
+
graphify update .
|
|
96
|
+
npx rightmodeler report --code-graph graphify-out/graph.json --repo .
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`init --code-graph <path>` renders the same section at the end of a run. For each call site the scanner found, the section lists the enclosing symbol, its callers, the tests that reach it, and the owners of those files, which are listed only. It also lists files that import an AI SDK where the scanner found no call site.
|
|
100
|
+
|
|
101
|
+
Graph edges are never replay trials, runtime proof, or quality evidence, and the flag never changes a stage before the report, a verdict, a gate, or confirmation. Each finding is labelled EXTRACTED, or INFERRED or AMBIGUOUS to verify, by its weakest hop. A graph built at another commit is shown file-level with a stale note. An unusable graph produces one warning, the section says why it is not shown, and the rest of the report is unchanged. The scan ignores `graphify-out/`, so building a graph never makes finished stages stale. Only `graphify update` and `graphify extract --code-only` are needed; other Graphify commands can call a language model.
|
|
102
|
+
|
|
103
|
+
`apply --code-graph <path>` appends the same section to the draft pull request body, limited to the call sites the pull request swaps and to five findings of each kind per call site. Owners there are listed only and are never requested as reviewers; reviewers still come from CODEOWNERS and blame. The graph never changes the swap, its digest, or its reviewers. `apply --dry-run` prints the exact body it would post, with or without `--code-graph`.
|
|
104
|
+
|
|
105
|
+
## Release policy
|
|
106
|
+
|
|
107
|
+
`--policy <path>` is accepted by `init`, `estimate`, `replay`, and `confirm`. The JSON object can set the quality floor, shortlist size, and model allow and deny lists:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"qualityFloor": 0.9,
|
|
112
|
+
"shortlistTop": 5,
|
|
113
|
+
"allowModels": ["acme/small-1"],
|
|
114
|
+
"denyModels": ["acme/large-1"]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`qualityFloor` must be greater than 0.8 and less than 1, and `shortlistTop` must be a positive integer. Changing the policy changes the stamped gate policy version, so shortlist and replay run again instead of pooling evidence gathered under the old policy.
|
|
119
|
+
|
|
120
|
+
## Catalogs without pricing or capabilities
|
|
121
|
+
|
|
122
|
+
Rightmodeler reads per-token pricing from the model catalog. When every catalog
|
|
123
|
+
entry has null pricing and no `--pricing-file` is set, it requests LiteLLM
|
|
124
|
+
`GET /model/info` on the same host. Use `--pricing-file` for bare OpenAI
|
|
125
|
+
endpoints or when `/model/info` has no usable per-token costs; file entries
|
|
126
|
+
override provider pricing.
|
|
127
|
+
|
|
128
|
+
Some gateways answer `/v1/models` with bare model ids: Envoy AI Gateway lists
|
|
129
|
+
the models a route declares, and Bifrost lists custom providers with ids and
|
|
130
|
+
context only. Pass `--catalog-reference <url|path>` to name the upstream's own
|
|
131
|
+
model list, for example `https://ai-gateway.vercel.sh/v1/models` or
|
|
132
|
+
`https://openrouter.ai/api/v1/models`. Rightmodeler reads it without your key or
|
|
133
|
+
headers and joins it to the gateway's entries by id, or by the reference id a
|
|
134
|
+
gateway id ends with (`openai/gpt-4o-mini` for `vercel/openai/gpt-4o-mini`). A
|
|
135
|
+
gateway entry takes from its match only what it does not declare itself (price,
|
|
136
|
+
context window, output ceiling, tool and structured-output support); a match
|
|
137
|
+
that is not a language model removes the entry; `--pricing-file` values win over
|
|
138
|
+
both. Entries still unpriced after the join are named in a
|
|
139
|
+
`catalog_reference_unmatched` warning, and a reference that cannot be read stops
|
|
140
|
+
the run with `invalid_catalog_reference`.
|
|
141
|
+
|
|
142
|
+
The release date is the one field where the reference wins over the gateway:
|
|
143
|
+
when the matched reference entry has a release date, it replaces the date the
|
|
144
|
+
gateway declares, and the gateway's date is kept only when the reference gives
|
|
145
|
+
none. Rightmodeler ranks judges partly by how recent a model is, and a gateway
|
|
146
|
+
can give every model the same placeholder date (Bifrost does for Vercel AI
|
|
147
|
+
Gateway's models), which would leave context and price to rank the judges and
|
|
148
|
+
favor the most expensive.
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
npx rightmodeler estimate --base-url https://provider.example/v1 --pricing-file /path/to/pricing.json --repo /path/to/repository
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The pricing file maps each model id to input and output USD per token and may
|
|
155
|
+
include the model's output ceiling:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"acme/model": {
|
|
160
|
+
"input": 0.000001,
|
|
161
|
+
"output": 0.000002,
|
|
162
|
+
"maxOutputTokens": 4096
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Without usable pricing from the catalog, a catalog reference, LiteLLM
|
|
168
|
+
`/model/info`, or a pricing file, the run refuses with `no_priced_candidates`
|
|
169
|
+
instead of reporting zero cost. The judge must be priced too, so price at least
|
|
170
|
+
one model from a family other than the current model's and the candidate's.
|
|
171
|
+
|
|
172
|
+
## Which model answered
|
|
173
|
+
|
|
174
|
+
Rightmodeler checks every replay and judge response, and in Mode B every response to a step whose model it sets, before it counts. It records the model the response names, and it leaves a response out of the evidence when that model is not the one it asked for, when a gateway reports that it answered from its cache (Portkey's `x-portkey-cache-status: HIT`, Bifrost's `cache_debug.cache_hit`), or when a gateway reports that it changed the request (a Portkey hook with `transformed: true`, Bifrost's compat plugin dropping parameters). Such a response is recorded with `attribution: "substituted"`, is never graded, and counts as `attribution_substituted`; more than 5% of a family's replays substituted abstains the family. A `replay_responses_substituted` warning counts them.
|
|
175
|
+
|
|
176
|
+
A response names the requested model when it echoes it, when it drops a gateway provider prefix (`openai/gpt-4o-mini` for `vercel/openai/gpt-4o-mini`), or when it adds a dated snapshot (`gpt-4o-mini-2024-07-18` for `gpt-4o-mini`). An alias whose answering model has another name counts as substituted, so name replay models by their upstream ids and turn off fallbacks, model aliases, response caching and request plugins for the replay route. Completed replay cells are reused, so after fixing the route rerun with a fresh store (`--store <directory>`). For streamed Mode B calls only the model a stream names and the response headers are checked.
|
|
177
|
+
|
|
178
|
+
A judge whose response names another model is retired at that first response: rightmodeler starts no new call to it, cancels its calls still waiting for budget, and re-judges the affected replays with the next-ranked judge once the calls already under way return; the `judge_unusable` warning names the model that answered. Some catalogs also list a faster service tier of a model under its own id, which the gateway answers as the base model: on Vercel AI Gateway, `<id>-fast` is `<id>` at its fast tier. Rightmodeler never ranks such an id as a judge or shortlists it as a candidate while `<id>` is in the catalog too; an id that ends in `-fast` with no base model listed, such as `morph/morph-v3-fast`, is treated like any other model.
|
|
179
|
+
|
|
180
|
+
## Gateways that route by header
|
|
181
|
+
|
|
182
|
+
Some gateways choose the upstream, the cache policy, or a trace tag from request headers. Pass each one with `--header 'name: value'`; repeat the option for more. Rightmodeler sends them with every request it makes to the provider base URL: the model catalog, candidate replays, judge calls, and the calls Mode B makes from your application, where a configured header replaces one your application sends. `authorization` comes only from `--api-key-env`, and `content-type`, `content-length` and `host` are set by rightmodeler, so none of them can be passed as a header. Completed replay cells are reused when the headers change, so after changing the route rerun with a fresh store (`--store <directory>`). A detached replay run is keyed to the header values by their SHA-256 digests; the values themselves are passed to the detached worker on its command line and are not written to the store. Do not put secrets in headers.
|
|
183
|
+
|
|
184
|
+
The default store is `.rightmodeler/` inside the analyzed repository. Completed stages resume when their inputs and outputs are still current. A complete run writes `.rightmodeler/project/reports/report.md`. The JSON report is kept inside the versioned store and is never written as a plain file, so read the final `result` event from `--output json` or `--output jsonl` for the machine-readable outcome.
|
|
185
|
+
|
|
186
|
+
Read the generated [command reference](commands.md), the [evaluator guide](evaluators.md), [Mode B configuration](modeb.md), the [gateway guide](gateways.md), and the [exit-code convention](exit-codes.md) before automating a full run.
|
|
62
187
|
|
|
63
|
-
|
|
188
|
+
To open the draft pull request and keep it reconciled, read the [GitHub guide](github.md).
|
|
64
189
|
|
|
65
|
-
|
|
190
|
+
Run `rightmodeler docs <name>` to print any of these documents from the installed package.
|