kanros migrate
Translate an existing promptfoo configuration (promptfooconfig.yaml)
into a kanros configuration in one command.
kanros migrate <INPUT> [--output <PATH> | --stdout] [--strict]
| Flag / arg | Default | Meaning |
|---|---|---|
<INPUT> | required | Path to the legacy config to migrate. |
-o, --output | ./kanros.yaml | Write to this path. |
--stdout | off | Print to stdout (mutually exclusive with --output). |
--strict | off | Exit 1 if any assertion could not be migrated (the file is still written). Use it in CI. |
What it does
kanros migrate reads the input file, parses the legacy schema, and
emits an equivalent kanros YAML. The translator:
-
Maps
providers:1:1 and translates each provider'sconfig:block (see Provider config below). -
Maps
prompts:1:1, converting anyfile:///https://URIs as-is. -
Carries
tests:file references over:tests: file://cases.csvand- file://more.jsonllist entries are kept (bare paths such astests: cases.csvgain afile://prefix), since kanros loads tests from files natively. Globs and non-data sources (e.g.file://gen.py) are kept with a# TODO(migrate):comment. -
Walks every
tests[i].assert[j]and translates each assertion to its kanros equivalent — renaming fields where only the spelling differs (regexvalue:→pattern:,contains-anyvalue:→values:, …) and rewriting inversions kanros spells differently (see Assertions). -
Translates
defaultTest→default_test, including itsvars:and appendedassert:list. -
Inserts
# TODO(migrate):comments at the top of the output wherever a promptfoo construct has no direct kanros equivalent, including one line per assertion that could not be migrated. -
Never leaves a test with zero checks. If every assertion of a test (or of
defaultTest) had to be dropped, a failing placeholder takes their place, so the test fails with aTODO(migrate)reason until you port the checks by hand:assert: - type: javascript code: 'return { pass: false, reason: "TODO(migrate): this test''s promptfoo assertion(s) `is-valid-openai-tools-call` could not be migrated automatically, ..." };' metric: migrate-todoA test that had no assertions in promptfoo either is listed in the summary:
kanros runrecords such cells as errors (see kanros run).
The command prints a summary line on success:
✓ migrated → kanros.yaml (3 providers, 5 prompts, 24 tests)
If TODOs were inserted, the line also notes the count:
✓ migrated → kanros.yaml (3 providers, 5 prompts, 24 tests) (2 TODO comment(s) — review before running)
A migration summary follows on stderr (so --stdout output stays a clean
config), listing every dropped assertion by location:
migration summary: 3 test(s), 1/3 assertion(s) migrated, 2 dropped, 1 failing placeholder(s) inserted where a test lost every assertion
dropped: tests[0] ("only unsupported").assert[0] `is-valid-openai-tools-call`: no kanros equivalent
dropped: tests[1] ("mixed").assert[1] `not-similar`: no kanros equivalent (kanros has no inverted form of `similar`)
warning: 1 test(s) have no assertions — `kanros run` reports them as errors: tests[2] ("no checks")
With --strict, a non-empty dropped: list makes the command exit 1.
Examples
kanros migrate legacy-config.yaml
kanros migrate legacy-config.yaml --output configs/regression.yaml
kanros migrate legacy-config.yaml --stdout | less
kanros migrate legacy-config.yaml --strict # CI: fail if anything was dropped
Provider config
promptfoo keeps sampling settings and credentials in a camelCase config:
block; kanros uses snake_case and lifts generation parameters to the
provider entry itself. Given:
providers:
- id: openai:chat:gpt-4o-mini
config:
apiKey: '{{ env.OPENAI_API_KEY }}'
apiBaseUrl: https://api.openai.com/v1
temperature: 0
maxTokens: 256
topP: 0.9
stopSequences: ["END"]
responseFormat: { type: json_object }
frequency_penalty: 0.5
kanros migrate emits:
# TODO(migrate): provider `openai:gpt-4o-mini`: config key `frequency_penalty` is not supported by the kanros provider and was dropped.
providers:
- id: openai:gpt-4o-mini
temperature: 0
top_p: 0.9
max_tokens: 256
stop:
- END
response_format:
type: json_object
config:
api_key: env:OPENAI_API_KEY
base_url: https://api.openai.com/v1
The rules:
| promptfoo | kanros |
|---|---|
temperature, seed | same key, lifted to provider level |
maxTokens, max_tokens, max_completion_tokens | max_tokens (provider level) |
topP, top_p | top_p (provider level) |
stop, stop_sequences, stopSequences | stop (provider level) |
responseFormat, response_format | response_format (provider level) |
apiKey | config.api_key |
apiBaseUrl | config.base_url |
'{{ env.NAME }}' in a value | env:NAME |
provider label: | dropped, with a # TODO(migrate): line |
| any other key the kanros provider does not accept | dropped, with a # TODO(migrate): line |
Provider ids
| promptfoo id | kanros id |
|---|---|
echo | echo:identity (offline, no API key) |
openai:chat:<m>, openai:completion:<m>, openai:responses:<m> | openai:<m> |
anthropic:messages:<m>, anthropic:completion:<m> | anthropic:<m> |
azure:chat:<deployment>, azureopenai:chat:<deployment> | azure-openai:<deployment> |
ollama:chat:<m>, ollama:completion:<m> | ollama:<m> |
togetherai:<m> | together:<m> |
cloudflare-ai:chat:<m> | cloudflare:<m> |
huggingface:text-generation:<m> | huggingface:<m> |
llama:<m> (llama.cpp) | llama-cpp:<m> |
any other <family>:chat:<m> / <family>:completion:<m> | <family>:<m> |
exec: <command> | subprocess:<name> with config.command set — TODO: adapt the script to the subprocess contract |
file://bot.py, python:bot.py | subprocess:bot running python3 bot.py — same TODO |
https://… (generic HTTP), ws://… | kept, with a TODO (no automatic equivalent) |
| a family kanros does not ship | kept, with a TODO |
What survives translation
| Source concept | kanros equivalent |
|---|---|
providers: list | providers: list (ids mapped as above) |
prompts: list (string) | prompts: list (string) |
prompts: list (file URI) | prompts: list (string, URI preserved) |
tests: list | tests: list |
tests: file://cases.csv | tests: file://cases.csv (CSV/JSONL/YAML/JSON loaded natively) |
tests: list with file:// entries | same list; file entries kept in place |
tests: huggingface://… | tests: huggingface://… |
vars: per test | vars: per test |
defaultTest | default_test |
threshold: per test | threshold: per test |
weight: / metric: on assertions | carried over unchanged |
Assertions
promptfoo type: | kanros |
|---|---|
equals, not-equals, contains, icontains, not-contains, starts-with | same type, value: kept (numbers/booleans become strings) |
regex, not-regex | same type, value: → pattern: (must compile as Rust regex: no lookarounds/backreferences) |
contains-any, contains-all | same type, value: → values: (a "a, b" string is split on commas) |
not-contains-any | contains-none (value: → values:) |
icontains-any / not-icontains-any | regex / not-regex with (?i)(?:a|b) |
icontains-all | assert-set (op: and) of icontains |
not-contains-all, not-icontains-all | assert-set (op: or) of the inverted checks |
not-icontains, not-starts-with | not-regex ((?i)<text>, ^<text>) |
is-json (no value:) | is-json |
is-json with a schema value: | json-schema (value: → schema:) |
contains-json, is-xml, is-sql | same type; an extra value: constraint is dropped with a TODO |
latency | latency (threshold: → max_ms:, rounded up) |
cost | cost (threshold: → max_usd:) |
perplexity | perplexity (threshold: → max:) |
levenshtein | levenshtein (value:/threshold: → reference:/max_distance:) |
similar, similar:<metric> | similarity (value: → reference:, provider: → embedder:, default threshold: 0.75) |
llm-rubric, model-graded-closedqa, answer-relevance | same type, grader provider: filled in (see below) |
g-eval, search-rubric | llm-rubric (pass/fail; threshold: dropped) |
factuality, context-recall, context-precision | same type, value: → reference: |
context-faithfulness | faithfulness with context: "{{ context }}" |
bleu, meteor, rouge-n | same type, value: → reference: (default thresholds; rouge-n gets n: 1) |
select-best | select-best (value: → rubric:) |
moderation | moderation (value: → categories:) |
classifier | classifier (provider: → model:, value: → label:) |
webhook | webhook (value: → url:) |
javascript | javascript (expression rewritten to return (…) over ctx.*; runs in boa_engine) |
python with file://script.py | python (script: path; context JSON on stdin) |
assert-set | assert-set (assert: → assertions:; threshold: → op: weighted) |
anything else (inline python, value: file://…, object equals, other not-* inversions, …) | dropped and listed; a failing placeholder if it was the test's last check |
Model-graded assertions (llm-rubric, factuality, context-*, …) require an
explicit grader provider: in kanros. promptfoo falls back to an implicit
default grader when the field is omitted — the migrator fills in the config's
first provider instead and flags it with a # TODO(migrate): comment so you
can pick a different grader deliberately. Bare provider strings
(provider: openai:gpt-4o) are wrapped into the { id: … } form kanros uses.
What needs human review
The migrator writes a # TODO(migrate): comment at the top of the output
for every construct that does not have a clean kanros equivalent (dropped
provider config keys, implicit graders, …). Look for these markers and resolve
them before piping the file into kanros run:
- Inline JavaScript graders that depend on Node.js built-ins
(
fs,path,http). kanros runs JS via embeddedboa_engine, which exposes no Node APIs. Rewrite the grader as a pure function or port it to a WASM plugin (see WASM plugins). - Custom dataset loaders. kanros loads
tests:from CSV, JSONL, YAML/JSON and Hugging Face datasets. Glob patterns (file://tests/*.yaml) and script or spreadsheet sources (file://gen.py, Google Sheets) are flagged with a TODO: list each file explicitly, or export the cases to one of the supported formats. - Cloud-only features — team mode, remote run history. kanros does
not have a server side; for run comparisons see
kanros diff, for sharing seekanros share. - Cost / latency assertions with units other than USD / ms. kanros is strict on units.
Sanity workflow after migration
kanros migrate legacy-config.yaml
grep -n 'TODO' kanros.yaml # resolve all TODOs
kanros validate --config kanros.yaml # check shape
kanros run --config kanros.yaml --no-cache # smoke test
Then start iterating.
Notes
- The migrator is one-shot. It does not watch the source for changes; re-run it after any meaningful edit to the source config.
- The output is deterministic given a fixed input. Diff-friendly.
- If the input file cannot be read or the YAML parser rejects it,
migrateexits with code2and prints the underlying error.