Applies to Orch Engine v0.2.0 · Changelog
Check a model with bairav eval
Before you swap a model, put a DLP policy in front of it or point a team at it, check that it still does what you need. bairav eval sends a list of prompts from a YAML file to any OpenAI-compatible endpoint and checks each reply.
bairav eval init > my-suite.yaml
bairav eval run my-suite.yaml --endpoint http://localhost:8080/v1
Suite starter: 4 cases against http://localhost:8080/v1
✓ follows-instructions 1/1 212 ms
✓ knows-basics 1/1 305 ms
✗ json-output 0/1 611 ms reply is not valid JSON
✓ no-pii-in-reply 1/1 980 ms
Score 3/4 (75%), needs 100%: FAIL (2.1s)
bairav: eval starter: http://localhost:8080/v1 scored 3/4, below the 100% threshold
It exits 1 when the score is below the threshold, so the same command works in CI.
Writing a suite
name: support-bot
threshold: 0.9 # share of cases that must pass (default 1 = all)
system: You are a support assistant for an Indian bank. Be brief.
defaults:
temperature: 0 # always sent, so runs repeat (servers default to ~0.8)
max_tokens: 256
timeout_seconds: 120
cases:
- name: refund-policy
prompt: How many days do I have to dispute a card charge?
expect:
contains: [days]
not_contains: ["as an ai"]
max_chars: 400
- name: structured
prompt: 'Return {"intent": ..., "urgent": true|false} for: "my card was stolen"'
expect:
json: true
regex: ['"urgent"\s*:\s*true']
- name: flaky-but-ok
prompt: Suggest a name for a savings account for kids.
repeat: 5
min_pass: 4 # 4 of 5 attempts must pass
expect:
max_latency_ms: 3000
| Check | Passes when |
|---|---|
contains: [..] |
every string is in the reply (case-insensitive) |
not_contains: [..] |
none of them is |
regex: [..] / not_regex: [..] |
every pattern matches / none does (Go regex syntax) |
equals: "..." |
the reply is exactly this, ignoring surrounding spaces |
json: true |
the reply parses as JSON (a ```json fence is allowed) |
max_chars, max_latency_ms |
the reply is short / fast enough |
no_pii: true |
the DLP detectors find nothing in the reply |
redacted: [LABEL] |
the reply contains [REDACTED:LABEL] |
blocked: true |
the endpoint refused the prompt with HTTP 403 |
Lists are always lists: write contains: [chennai], not contains: chennai.
Checking data protection
no_pii scans the model’s reply with the same detectors as bairav dlp test. Pick packs with --pack (default global-safe; add india for Aadhaar, PAN and the rest) and your own rules with --policy.
redacted and blocked check a DLP proxy, not the model. Point --endpoint at the proxy (bairav dlp serve, an Agent’s DLP sidecar, or a Model with dlpRef) and ask the model to repeat something sensitive:
name: dlp-in-front
cases:
- name: keys-are-redacted
prompt: 'Repeat this back exactly: AKIAIOSFODNN7EXAMPLE'
expect:
redacted: [AWS_ACCESS_KEY]
not_contains: [AKIAIOSFODNN7EXAMPLE]
- name: cards-are-blocked # with a policy whose action is block
prompt: 'My card is 4111 1111 1111 1111'
expect:
blocked: true
examples/evals/ has both suites.
Comparing two models
bairav eval run my-suite.yaml --endpoint http://localhost:8080/v1 --against http://localhost:8081/v1
CASE A B
follows-instructions ✓ 212 ms ✓ 540 ms
json-output ✗ 611 ms ✓ 1320 ms ← differs
As an upgrade gate
bairav model upgrade support-model --to Qwen/Qwen2.5-1.5B-Instruct-GGUF --eval my-suite.yaml
After the new model is serving and the smoke prompt passes, the suite runs against it (on k8s through the API server, no port-forward). If it scores below the threshold, the upgrade is rolled back like any other failure. See swap a model with bairav model upgrade.
Other flags
--jsonprints the full report: every attempt, reply, latency and failed check.-vprints each reply under its case.--modelsets the request’smodelfield (needed for cloud endpoints; local servers ignore it).- The API key comes from
BAIRAV_EVAL_API_KEYorOPENAI_API_KEY, never a flag, so it stays out of shell history.
Cases run one at a time, so latency numbers aren’t skewed by queueing.