EvaltDocs

Evalt Python SDK

Python SDK reference.

Install Evalt, define durable routes, validate suites, control provider spend, inspect promotion decisions, and enforce measured quality in CI.

PyPIpip install evalt
Five-minute integration

Run one durable route

Set OPENROUTER_API_KEY in your environment or a .env file in the directory where you run Python. Evalt() finds it automatically; an explicit api_key= takes precedence. The key is never written to Evalt’s route database or result files.

Python
from evalt import Evalt

ticket = "Please help—the website won't load."
evalt = Evalt()  # interactive terminals show design, tournament, route, and cost

answer = evalt.run(
    "Classify this request. Return exactly one lowercase label: billing, account, or technical.",
    ticket,
    task="Route recurring support tickets to billing, account, or technical.",
    route="support-routing",
    target_accuracy=0.95,
    test_budget_usd="auto",
)

print(answer.content)

Connect local routes to the hosted workspace

The SDK remains the production runner. The optional workspace gives its routes a visual history and live progress view without moving customer content into the dashboard.

Connect this computer once
python3 -m evalt connect

The command creates one private workspace token in your user-level Evalt config, opens evalt.dev/app, and publishes existing sanitized route summaries from the current .evalt/evalt.db without making a model call. That connection follows scripts across project folders; pass --state path/to/evalt.db only when one project needs a separate workspace. The CLI and browser show a safe ws_... ID; those IDs must match. Future Evalt.run(...) calls publish bounded route progress and final operational summaries, and the terminal explicitly reports whether each sync succeeded. Every invocation has an opaque run ID and an explicit running, completed, or failed state, so two tournaments on the same route never blend together. Prompts, inputs, outputs, cases, provider keys, request bodies, and raw responses stay local. Dashboard outages never fail a production call.

python3 -m evalt dashboardOpen the workspace connected to this Python installation.
python3 -m evalt --statusShow the safe workspace ID without opening a browser; the longer dashboard --status form also works.
python3 -m evalt doctorCompare the imported package, Python executable, PATH console shim, local routes, hosted routes and workspace reachability without provider calls.
python3 -m evalt dashboard --sync-existingRecover current sanitized route summaries without rerunning a tournament.
python3 -m evalt disconnectRemove the local connection. This does not change a serving route or delete remote metadata.
EVALT_WORKSPACE_TOKENUse an environment variable instead of the local config file in CI or a deployed service.

The first evw_... capability is the owner key. Keep it out of CI and shared channels. Open Workspace access in the dashboard to create expiring evc_... Viewer, Publisher, or Editor capabilities. The CLI reports the effective role and permissions without printing the capability.

Interactive terminal progress (stderr)
Evalt · support-routing · TEST DESIGN STARTED · 25 cases · current catalog-selected designer · deadline 120s · one workflow cap $1.00
Evalt · support-routing · DESIGNING TESTS · openai/gpt-5.4-mini · 10s elapsed · still working
Evalt · support-routing · TEST DRAFT READY · 25 AI-authored cases · $0.94 remains for the tournament
Evalt · support-routing · BROAD SCREEN · 8 model configuration(s) · up to 8 in parallel
Evalt · support-routing · SCREENED · qwen/qwen3.5-9b#reasoning=low · 100% validation · p90 920 ms · $0.041200 spent
Evalt · support-routing · ROUTE SELECTED · qwen/qwen3.5-9b#reasoning=low · 100% observed final test · $0.31 test spend
Evalt · support-routing · PROVISIONAL EVIDENCE · 74.1% one-sided 95% lower bound · 10 distinct scenarios
What happens on the first call?Evalt uses the real input only as an unlabeled example of workload shape, creates and calibrates a separate 25-case AI test, searches prompts, training-only few-shot packages, models, providers, and supported reasoning efforts, promotes only a final-test passing package, then answers that real input through it. You do not call accept() or correct() to start this tournament; those methods optionally label later production outcomes. The route remains labeled AI_GENERATED_AI_JUDGED until production feedback strengthens the contract. Use first_run="bootstrap" only for an explicitly untested single call.
Optional review boundary

Inspect the AI-built test before it runs.

Evalt selects a capable test designer from the current intelligence-and-price catalog, then drafts 25 varied routine, ambiguous, adversarial, boundary, and realistic-domain scenarios in five parallel batches before the split. It recommends exact, semantic, or explicit numeric-tolerance judging. A 0–100 subjective rating can therefore accept a nearby approved score without asking another model to pretend one arbitrary number is uniquely correct. The designer fallback chain is a separate live-catalog shortlist and never borrows the judge. The draft spends from the same workflow cap as the later tournament. Evalt names the active model and attempt; if a provider returns malformed structured output, it rejects that draft and makes one visible budget-bounded recovery attempt before falling back.

AI drafts, you approve
from evalt import Evalt

evalt = Evalt()
draft = evalt.optimize_task(
    task="Route recurring support tickets to billing, account, or technical.",
    prompt="Return exactly one lowercase label: billing, account, or technical.",
    route="support-routing",
    case_control="review",
    workflow_budget_usd=1.00,
)

draft.save("support-routing-draft.json")
for case in draft.examples:
    print(case.id, case.conversation())
if input("Type APPROVE after reviewing every expected output: ").strip() != "APPROVE":
    raise SystemExit("Draft saved; no tournament ran.")

# Explicit trust boundary; pass edited examples to approve(...) when needed.
suite = draft.approve()
result = evalt.run(suite)
case_control="review"

AI drafts, you approve

Use for high-stakes or tightly specified work. AI proposes cases and expected behavior; none counts until approve().

Evalt.run(...)

Automatic first route

The default creates, calibrates, judges, and runs the suite immediately, with permanent AI_GENERATED_AI_JUDGED provenance.

After approval, the tournament tries current model candidates and supported reasoning levels in parallel. Prompt rewrites and approved training-only few-shot packages compete too. With the default balanced 25-case suite, ten cases train prompt packages, five choose among them, and ten unique unseen cases confirm the frozen winner. Each final case runs twice, for twenty final executions; prompt tuning never sees them.

Python API

Route lifecycle

A route is one stable production task with its own prompt, examples, budget, selected configuration, and maintenance history.

Evalt.run(..., route=name)On a new route, design and test a package before answering; afterward, reuse the promoted package and return a feedback-capable answer.
Evalt.run(..., route_version=id)Serve that exact current qualified package or raise RouteVersionMismatch before provider spend. Use this in deployed services.
Evalt.run(..., reoptimize=True)Deliberately run a new qualifying test for an existing text route. The current package stays in place unless the replacement clears the frozen gates.
answer.contentRead the production output before recording a verdict.
answer.accept()Store this input and output as approved evidence for this route only.
answer.correct(expected)Store the corrected expected behavior without changing another route.
evalt.route_status(name)Inspect the promoted model, prompt version, evidence count, budget, and maintenance reasons.
Inspect a route
status = evalt.route_status("support-routing")

print(status["selected_model"])
print(status["current_package_id"])
print(status["selected_prompt_version"])
print(status["target_accuracy"])
print(status["test_budget_usd"])
print(status["maintenance_due"])
Run independent tasks
support = evalt.run(support_prompt, ticket, route="support-routing")
summary = evalt.run(summary_prompt, transcript, route="call-summary")
fraud = evalt.run(risk_prompt, transaction, route="fraud-review")

Each route keeps separate prompts, evidence, winners, budgets, and retest history. Feedback never crosses route names.

OpenRouter compatibility

Test the request you will actually serve.

Put Chat Completions settings in request_options. Evalt sends the same canonical envelope to every target-model training, validation, and final-test call, stores its SHA-256 fingerprint with the winning route, and reuses it when later calls omit the argument. Test design, prompt optimization, and judging keep independent settings.

Structured output, tools, sampling, and provider routing
answer = evalt.run(
    prompt,
    ticket,
    route="support-routing",
    max_tokens=2048,
    request_options={
        "temperature": 0.2,
        "top_p": 0.95,
        "response_format": {
            "type": "json_schema",
            "json_schema": {
                "name": "support_route",
                "strict": True,
                "schema": route_schema,
            },
        },
        "tools": tools,
        "tool_choice": "auto",
        "parallel_tool_calls": False,
        "provider": {
            "require_parameters": True,
            "data_collection": "deny",
            "zdr": True,
        },
        "plugins": [{"id": "response-healing"}],
    },
)

print(answer.content)
print(answer.tool_calls)
print(answer.request_envelope_validated)
request_options={...}Accepts current and future JSON-serializable Chat Completions fields, including structured output, sampling controls, penalties, stop sequences, tools, prediction, provider routing, plugins, web search, verbosity, and transforms.
max_tokens=...Evalt owns the output ceiling and tests it as part of the route contract. Omit it later to reuse the tested value.
answer.tool_callsReturns normalized function calls without treating a content-free tool response as an empty completion.
strict_request_options=TrueReject a production override before provider spend. Without strict mode, Evalt allows it but emits RequestEnvelopeDriftWarning and marks the answer unvalidated.
route_status(name)Shows the persisted envelope, output ceiling, fingerprint, and every recorded drift event.
Evalt-owned fieldsmodel, fallback models, messages, prompt, usage, streaming lifecycle, output-token fields, and reasoning effort cannot be hidden inside request_options. Evalt varies or accounts for those directly. Zero Data Retention, denied provider data collection, and required-parameter routing remain enforced safety defaults. The pass-through covers the Chat Completions runner, not separate image, embeddings, rerank, speech, or video endpoints.
Multimodal evaluation

Compare models on images you actually approved.

Evalt accepts local PNG, JPEG, WebP, and GIF files or public HTTPS image URLs whose path ends in one of those extensions through the Chat Completions image-understanding path. It filters out text-only candidates before spend, keeps the image modality in the tested request contract, and never sends raw images, filenames, paths, source URLs, or thumbnails to the hosted dashboard.

You do not need AI to make the examples: choose images whose answers you already know, write the expected label yourself, and review the set before running it. AI-generated or automatically inferred image labels do not count as approved ground truth.

Import an existing labeled image table
evalt import-dataset package-labels.csv \
  --prompt "Inspect the package. Return exactly one label: damaged or intact." \
  --input-text "Inspect this image." \
  --image-field image_path \
  --expected-field label \
  --id-field case_id \
  --group-field condition \
  --evaluator exact_text \
  --model google/gemini-2.5-flash-lite \
  --model openai/gpt-5.4-nano \
  --confirm-expected-human-approved \
  --output package-condition.json

The importer reads CSV, JSONL, or a JSON array entirely offline. Mappings are exact; Evalt does not guess the input, expected answer, ID, group, weight, or image columns. Use --input-field text instead of --input-text when text differs by row. Image paths must be relative to the dataset file. The importer validates local PNG, JPEG, and WebP files, rejects URLs and directory traversal, refuses ambiguous mappings, duplicate IDs, malformed records, unsafe sizes, and existing outputs, then emits a deterministic evalt-suite-v2 file. Its machine-readable summary contains only counts and a hash—never prompts, rows, labels, paths, or images—and it does not read provider or dashboard credentials.

Human-approved image suite
from evalt import Evalt, Example, ImageInput, Suite, multimodal_input

examples = tuple(
    Example(
        multimodal_input("Return exactly one label: damaged or intact.", ImageInput.from_path(path)),
        approved_output=label,
        id=f"package-{index}",
    )
    for index, (path, label) in enumerate(approved_images, start=1)
)

suite = Suite(
    name="package-condition",
    prompt="Inspect the package image. Return exactly one label: damaged or intact.",
    examples=examples,
    models=("google/gemini-2.5-flash-lite", "openai/gpt-5.4-nano"),
    evaluator={"type": "exact_text"},
    optimize_prompt=False,
    max_optimization_cost_usd=0.25,
)

result = Evalt().run(suite)
print(result.winner.model, result.winner.holdout_pass_rate)
ImageInput.from_path(path)Validates the file signature and size locally, then creates the provider data URL in memory. The original file is never changed.
ImageInput.from_url(url)Accepts public HTTPS URLs without embedded username/password credentials when the URL path ends in .png, .jpg, .jpeg, .webp, or .gif. URL query data is never written to route call metadata.
multimodal_input(text, *images)Builds the ordered text-first multipart content used in every target-model case.
optimize_prompt=FalseRequired for image suites in this release. Evalt compares models against the frozen prompt and labeled images instead of pretending an automatic text-only prompt writer saw the evidence.
Exact boundaryThis is image understanding in Chat Completions, not image generation. Automatic first-route case design does not invent image labels. Image-bearing feedback is recorded as non-replayable unless the customer supplies an explicit approved suite; raw media never enters hosted route metadata.
Evaluation lineage

Know which frozen cases produced every decision.

Turn an approved suite into an immutable local history. Each revision names its exact parent, requires stable case IDs, and records whether cases were added, removed, changed, or left untouched. A stale base fails instead of silently replacing another reviewer's work.

Create, revise, compare, export
evalt dataset create support support-v1.json \
  --message "Initial human-reviewed cases" \
  --tag reviewed

evalt dataset revise support support-v2.json \
  --base dv_0123456789abcdef0123456789abcdef \
  --message "Correct labels and add refund edge cases"

evalt dataset diff support \
  --from-version dv_0123456789abcdef0123456789abcdef

evalt dataset export support \
  --output support-production.json
dataset createValidate and freeze one Evalt suite as the first immutable version.
dataset reviseFreeze a complete successor suite only when --base is still the latest version.
dataset diffShow added, removed, changed, and unchanged counts. Case IDs remain hidden unless --show-case-ids is explicit.
dataset list / showVerify local history and print bounded metadata without prompts, cases, expected answers, or images.
dataset exportRecover any exact historical version as a runnable evalt-suite-v2 file.
Private by constructionThe registry defaults to .evalt/datasets, never reads provider or dashboard credentials, and never synchronizes content or version metadata. Objects and the index are integrity-checked; Evalt exposes no delete or history-rewrite command.
Human evidence

See where the judge is wrong before trusting its score.

Create a private queue from one frozen suite and one matched set of judge decisions. Reviewers claim explicit cases, record a human pass or fail, and can skip or reopen an item without losing the queue's event history. Calibration then separates true passes, false passes, true failures, and false failures instead of hiding disagreement inside one average.

Review first, calibrate second
evalt review create support support-production.json \
  --decisions judge-decisions.json \
  --rubric "Pass only when the approved behavior is fully satisfied."

evalt review claim support case-17 \
  --reviewer reviewer-a \
  --revision qr_1_0123456789abcdef

evalt review resolve support case-17 \
  --reviewer reviewer-a \
  --human-fail \
  --corrected-output-file corrected.txt \
  --revision qr_2_0123456789abcdef

evalt review calibrate support \
  --output judge-calibration.json
review createRequire an exact decision for every stable suite case ID, then freeze the private rubric, cases, judge pass/fail labels, and optional scores locally.
review claim / resolve / skip / reopenApply strict state transitions against an exact qr_... revision. A stale writer fails instead of replacing another reviewer's decision.
review list / showVerify queues and print progress without prompts, cases, approved outputs, corrections, or rubrics. One case is revealed only with explicit --include-content.
review calibrateReport agreement, false-pass and false-fail rates, confusion counts, coverage, and a score-threshold sweep. Case IDs remain hidden unless --show-case-ids is explicit.
review exportWrite the exact resolved decision set for later local analysis or qualification evidence.
No model call is hidden inside reviewAll evalt review commands are offline. The store defaults to .evalt/reviews, reads no provider or dashboard credential, and synchronizes no case, rubric, decision, correction, reviewer label, score, path, hash, or progress metadata.

false_pass_rate is the share of judge passes rejected by a human. false_fail_rate is the share of judge failures accepted by a human. A calibration report describes only the reviewed queue revision; it does not prove that one reviewer is universal ground truth. Run python -m evalt.examples.review_calibration for a complete packaged offline example.

Application-level evaluation

Test the function your users call—not only the model behind it.

Compare customer-owned Python functions, agents, retrieval code, or RAG pipelines against one frozen set of approved cases. Every system receives the same stable case identity and an isolated deepcopy-compatible input. Evalt records quality, failures, p50 and p90 latency, configured cost, and the lowest-cost system clearing the floor without making a provider call or uploading the cases.

Compare two local RAG pipelines
from evalt import CallableSystem, SystemExample, evaluate_systems

cases = (
    SystemExample(
        {"query": "How long are refunds available?", "documents": documents},
        "30 days",
        id="refund-window",
        critical=True,
    ),
)

result = evaluate_systems(
    cases,
    (
        CallableSystem("baseline", baseline),
        CallableSystem("rag-v2", rag_v2, cost_per_call_usd=0.0003),
    ),
    evaluator={"type": "exact_text"},
    quality_threshold=1.0,
)

print(result.selected_system)
result.save("application-eval.json")
SystemExampleFreeze structured Python input, approved output, stable ID, group, difficulty, weight, critical flag, and optional multi-turn contract.
CallableSystemRegister one explicit local sync or async callable with a deadline, output bound, configured per-call cost, and content-free metadata.
ProcessSystemRun a trusted local module:function entrypoint in a supervised child process that is terminated when its deadline expires.
evaluate_systemsRun bounded concurrent local comparison with exact-text, exact-JSON, numeric-tolerance, or explicit local custom scoring.
evaluate_systems_asyncUse the same contract inside an existing event loop without nesting asyncio.run.
SystemOutputReturn JSON-serializable content plus measured incremental cost and bounded metadata when one fixed cost is not enough.
Local code is always explicitEvalt never imports or executes a callable named by a downloaded suite or the dashboard. Use CallableSystem for in-process functions and ProcessSystem("rag-v2", "my_app.rag:answer") when a non-cooperative deadline must terminate the child. Process supervision is a lifecycle boundary, not an operating-system security sandbox; the local entrypoint must still be trusted.

Result export is content-free by default: it stores case IDs, decisions, metrics, and hashes instead of inputs or outputs. Use include_content=True only for an intentional private local artifact. The adapters read no provider or dashboard credential and perform no synchronization. Run python -m evalt.examples.callable_rag for the in-process example or python -m evalt.examples.process_supervision for the supervised-process example.

Route control plane

The SDK serves. The workspace explains why.

The free workspace is organized around durable named routes, with the latest local invocation scoped inside each route. Open one to see whether the current run is active, completed, or failed; its progress list excludes older invocations while the serving configuration and route history remain durable.

RoutesThe task portfolio: current quality, production cost, serving configuration, and last check.
EvidenceThe exact validation and final-test contract that a challenger must clear.
HistoryPromotions, rejections, prompt changes, price snapshots, and explicit approvals.
IntegrateThe stable route name production code calls instead of hard-coding today’s model.

Share the workspace without sharing the owner key

Workspace access remains accountless. An owner creates a named, expiring capability and Evalt shows its secret once. Access links keep the capability in the URL fragment, so it is not sent in the page request, server logs, or referrer. The server stores only a one-way capability digest plus the bounded label, role, creation, expiry, revocation, and last-used times.

ViewerList and inspect synchronized routes. Cannot publish, delete, or manage access.
PublisherPublish aggregate route metadata from the SDK or CI. Cannot list, read, delete, or manage access.
EditorList, inspect, and publish routes. Cannot delete dashboard copies or manage access.
OwnerFull route and access control. Existing evw_... workspaces remain compatible.
Revocation is immediateExpired or revoked delegated capabilities fail on their next request. Existing capabilities cannot create another capability or increase their own role. A workspace can have at most 20 active delegated capabilities.
Production learning

Accept, correct, or leave unjudged

Feedback is explicit. Evalt does not silently infer that a delivered answer was correct. Accept an outcome only when it meets the real task; correct it with the expected behavior when it does not.

answer.accept()Adds the input and returned output as approved evidence.
answer.correct(expected)Adds the input with the corrected expected output.
answer.contentThe production response; usable before feedback is recorded.
evalt.route_status(name)Returns the promoted route, evidence counts, price snapshot, and maintenance state.
evalt.route_versions(name)Lists immutable qualified local packages without a provider call.
evalt.annotate_route_version(name, id, alias=…, note=…)Adds a private local name or note without provider spend or dashboard sync.
evalt.rollback_route(name, version)Atomically restores a qualified package by exact ID or local alias and records the rollback.
Explicit optimization

Freeze a suite before provider spend

Use a suite when you already have approved cases, need an auditable tournament, or want a CI artifact. Validation is offline; optimization is the only command below that calls a provider.

python3 -m evalt init evalt.jsonCreate a documented starter suite.
python3 -m evalt validate evalt.jsonValidate schema and contracts offline.
python3 -m evalt optimize evalt.json --output evalt-result.jsonRun the bounded search and save JSON.
Install a reviewed Suite as a routeUse evalt.qualify_route(approved_suite, route="support-routing"). This is also the production installation path for human-labeled image suites. Qualified packages are versioned locally; image bytes and visual few-shot examples are not persisted.
Pin a deployment to its tested package
status = evalt.route_status("support-routing")
version = status["current_package_id"]

answer = evalt.run(
    prompt,
    input,
    route="support-routing",
    route_version=version,
)

Safe rollout: qualify locally, read the new current_package_id, deploy that pin, and deliberately replace it only after review. A stale, missing, cross-route, unqualified, or damaged pin fails before any provider call. Rollback creates a new current package ID, so deploy that new ID rather than silently reusing the old one.

python3 -m evalt versions --route support-routingInspect qualified local packages without a provider call.
python3 -m evalt annotate-version --route support-routing --version rv_… --alias known-good --note "Approved candidate"Add a private local name and note. Neither value is synchronized to the hosted dashboard.
python3 -m evalt rollback --route support-routing --version known-good --yesRestore by unambiguous local alias; the result always returns the canonical exact ID.
Names help humans; exact IDs protect deploymentsAliases are unique per route and accepted only for local rollback. Keep production route_version= pinned to the exact immutable rv_… ID so a renamed alias cannot change serving behavior.
Minimal suite JSON
evalt.json
{
  "schema": "evalt-suite-v2",
  "name": "support-routing",
  "prompt": "Return billing, account, or technical.",
  "examples": [
    {"id": "billing-1", "input": "Charged twice", "approved_output": "billing"},
    {"id": "account-1", "input": "Reset link expired", "approved_output": "account"},
    {"id": "technical-1", "input": "App freezes", "approved_output": "technical"}
  ],
  "evaluator": {"type": "exact_text"},
  "quality_threshold": 0.95,
  "max_optimization_cost_usd": 2.0,
  "rounds": 3,
  "max_parallel_models": 16,
  "max_parallel_scenarios": 32,
  "request_timeout_seconds": 600
}
Production-shaped evidence

Model the bell curve without hiding the hard tail

An overall 95% can be misleading when routine requests dominate traffic. Use group to stratify related cases across training, validation, and final test; weight to approximate production frequency; and difficulty_thresholds to require separate routine, complex, and adversarial floors.

Stratified suite excerpt
{
  "examples": [
    {"id":"routine-01", "group":"ordinary-refund", "difficulty":"routine", "weight":6, "input":"...", "approved_output":"approve"},
    {"id":"hard-01", "group":"policy-conflict", "difficulty":"adversarial", "weight":1, "critical":true, "input":"...", "approved_output":"manual_review"}
  ],
  "quality_threshold": 0.95,
  "difficulty_thresholds": {
    "routine": 0.95,
    "complex": 0.90,
    "adversarial": 0.85
  }
}

This is an excerpt. When grouping is enabled, every case needs a group and every group needs at least five cases. Promotion requires the overall weighted target and every named difficulty floor. Put rare catastrophic rules in a deterministic veto too.

Judging

Use the cheapest valid evaluator

Prefer deterministic evaluators whenever the contract allows it. Semantic judging is useful for nuanced outputs, but it adds provider cost and must be calibrated against route-specific human decisions before promotion.

exact_text

Strict normalized labels or strings. No judge-model call.

exact_json

Required keys, optional additional-property vetoes, and rational normalization. No judge-model call.

custom

Your versioned local rubric. The suite names an ID and version; trusted code is registered separately and costs no evaluator tokens.

semantic

Meaning-based verdicts for open outputs. Use an explicit evaluator model and approved rubric.

Custom scorer: identity in the suite, code registered locally
from evalt import CommandScorer, Evalt, Suite
import sys

suite = Suite.load("evalt.json")
scorer = CommandScorer(
    "domain-rubric", "1.0",
    [sys.executable, "tools/score.py"],
    timeout_seconds=5,
)
result = Evalt(
    custom_scorers={scorer.scorer_id: scorer}
).run(suite)

A suite can contain only type, scorer_id, and scorer_version for a custom evaluator. It cannot choose a command, module, executable, or path. Command scorers use strict JSON stdin/stdout, no shell, bounded I/O, a per-case timeout, and a minimal environment that excludes provider credentials by default. Missing registration, version drift, timeout, nonzero exit, malformed output, and non-finite scores stop the run.

Conversations

Keep a conversation in one split

A multi-turn scenario uses a turns array. Evalt keeps the full conversation together when creating training, validation, and final-test splits, and replays prior assistant context in order.

Multi-turn case
{
  "id": "refund-followup",
  "turns": [
    {"role": "user", "content": "I was charged after cancelling."},
    {"role": "assistant", "content": "When did you cancel?"},
    {"role": "user", "content": "Three days before renewal."}
  ],
  "approved_output": "billing"
}

Turns inside one scenario remain ordered. Independent scenarios and repeated executions may run concurrently.

Hard limits

One budget covers the whole search

max_optimization_cost_usd covers prompt improvement, candidate calls, judging, retries, and final testing. Evalt reserves estimated in-flight cost before dispatch; work that cannot fit does not start.

16default parallel model lanes
32default parallel scenario executions
600sdefault per-response deadline
1budget-checked retry for an empty or truncated response
131Kretry ceiling for hidden reasoning tokens

Raise concurrency to reduce wall time, not to expand spend. Model lanes are configurable up to 32 and independent scenario executions up to 128; every request still competes for the same reserved budget. The terminal reports the broad parallel screen, then prints each settled configuration's validation rate, p90 latency, spend, and total elapsed time. Automatic first-route tests generate five coverage batches concurrently and use a 120-second deadline for both designer and candidate requests. Set designer_request_timeout_seconds or test_request_timeout_seconds when the job needs different limits. A candidate that times out cannot earn a higher reasoning rung. Automatic routes try one prompt rewrite by default; set optimization_rounds from 1 to 8 for a deeper search. Explicit suites keep a configurable 600-second default and can raise request_timeout_seconds as high as 7200 seconds.

When the cap ends too soonThe result includes continuation_recommendation only when budget-limited configurations remain unfinished. It lists those configurations and a bounded suggested next cap. automatic_spend is always false; a rerun still requires explicit approval.
Native GitHub integration

Run the same performance gate on every trusted change

The versioned Evalt Action can run a budget-capped tournament or gate an existing result without provider calls. It validates the suite before spending, writes JSON, HTML, and JUnit evidence, and exposes stable outputs for later deployment steps.

.github/workflows/evalt.yml
name: Evalt

on:
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  performance-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Find the cheapest route that passes
        id: evalt
        uses: JarJarBeatyourattitude/evalt-action@v1
        env:
          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
        with:
          suite: evalt.json
          min-pass-rate: "0.95"
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: evalt-evidence
          path: |
            evalt-result.json
            evalt-report.html
            evalt-junit.xml
The spend cap lives in the suitemax_optimization_cost_usd covers target calls, prompt improvement, judging, and final testing. The Action does not silently increase it. Use optimize: "false" to apply the same gate to a saved result fully offline.

Store the OpenRouter key as a repository Actions secret. Ordinary secrets are not passed to workflows from forks; do not switch untrusted pull requests to pull_request_target merely to expose a provider key. See the Action repository for every input and output.

Private local evidence

Use stable names instead of fragile file paths

The local evidence library gives approved suites and exported results immutable names such as @support-v1 and @support-baseline-v1. Evalt validates every import, preserves the original UTF-8 JSON bytes, verifies the SHA-256 digest on every read, and keeps the catalog on this computer. Library names, tags, paths, hashes, suites, results, prompts, images, cases, and outputs are never synchronized to the hosted workspace.

Import once, reuse everywhere
evalt library add evalt.json \
  --name support-v1 \
  --tag production

evalt library add evalt-result.json \
  --name support-baseline-v1 \
  --tag baseline

evalt validate @support-v1
evalt check @support-baseline-v1 --min-pass-rate 0.95
evalt monitor @support-baseline-v1 \
  --route support-routing \
  --max-cost-usd 0.10
library addValidate and import one suite or result under a new immutable name. Repeat --tag for local organization.
library listFilter bounded metadata with --kind, --tag, or --query without printing customer content.
library showVerify one object and show its kind, size, tags, digest, timestamp, and safe summary only.
library resolveVerify one object and print its private content-addressed path for local tooling.
library exportWrite the exact verified original bytes. Existing different files are never replaced unless --force is explicit.

@name works anywhere the CLI reads a suite or result: validate, optimize, monitor, report, compare, and check. For a non-default catalog, add --library-root path to those commands or set EVALT_LIBRARY_HOME. The default is .evalt/library in the current project.

Immutable is deliberateA name cannot silently change its content or tags. Import a revision under a new name such as @support-v2; this keeps baselines reviewable and prevents a path edit from changing the evidence behind a deployment decision. The catalog is not a backup, so copy or export important evidence into your normal encrypted backup policy.

For image routes, import the original reviewed suite too, then run evalt monitor @support-baseline-v1 --suite @support-images-v1 .... The library stores the exact local suite; the dashboard still receives only the aggregate health verdict and deltas.

Frozen route health

Detect drift without changing the route

evalt monitor reruns only the selected prompt, model, few-shot package, evaluator, request options, final-test cases, and repeat count from an earlier result. It does not search, rewrite, promote, roll back, maintain, or otherwise change the serving route. A positive provider-spend cap is mandatory.

Explicit local recheck
python3 -m evalt monitor @support-baseline-v1 \
  --route support-routing \
  --max-cost-usd 0.10 \
  --max-regressions 0 \
  --max-quality-drop-pp 0 \
  --max-cost-increase-pct 15 \
  --max-p90-increase-ms 250 \
  --output evalt-monitor-result.json \
  --history .evalt/monitor-history.jsonl
0 · HEALTHYThe frozen configuration still clears the requested absolute and baseline gates.
1 · REGRESSIONThe measurement completed, but quality, a case, cost, or p90 latency crossed a configured limit.
2 · ERRORThe contract is invalid, the cap cannot authorize the next call, or the provider/runtime did not complete the check.

The full evalt-monitor-result-v1 file stays local and remains compatible with evalt check, evalt compare, HTML reports, and JUnit reports. Optional history is aggregate-only: timestamp, verdict, opaque suite hash, model, quality/cost/latency deltas, regression counts, and check spend. It never contains prompts, inputs, images, approved answers, model outputs, judge reasons, generation IDs, request bodies, or credentials. With --route and an existing workspace connection, only that aggregate verdict and those deltas synchronize to the route dashboard.

Image routes need the original local suiteResult receipts intentionally omit image bytes and signed URLs. Add --suite @reviewed-image-suite-v1 or a normal file path. Evalt verifies the suite's privacy-safe image descriptors and approved outputs against the frozen contract, and recurring checks require embedded local fixtures because an HTTPS image can change without its URL changing.

Opt-in scheduled notification

A scheduled GitHub workflow is the notification boundary: REGRESSION or ERROR fails the job and GitHub applies the repository's normal Actions notifications. The schedule is never enabled by Evalt, and every run consumes provider budget only up to the cap shown in the workflow.

.github/workflows/evalt-route-health.yml
name: Evalt route health

on:
  workflow_dispatch:
  schedule:
    - cron: "17 8 * * 1"

permissions:
  contents: read

jobs:
  frozen-recheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: python3 -m pip install --upgrade -r https://evalt.onrender.com/python-sdk/latest.txt
      - name: Recheck the frozen winner
        env:
          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
        run: |
          python3 -m evalt monitor evalt-baseline.json \
            --route support-routing \
            --max-cost-usd 0.10 \
            --max-regressions 0 \
            --max-quality-drop-pp 0 \
            --output evalt-monitor-result.json
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: evalt-route-health
          path: evalt-monitor-result.json

Run schedules only on a trusted branch. Commit a baseline only when its approved text cases are repository-safe. Keep private baselines and image suites in an encrypted runner secret or on a controlled self-hosted runner. HEALTHY means no configured regression was observed; it does not turn a small or provisional test set into a universal reliability guarantee.

Signed outbound decisions

Alert your systems without exporting evaluation content

Evalt can send a versioned aggregate event after a frozen route-health check or GitHub Action gate. The default payload contains an opaque route reference, verdict, suite hash, quality delta, regression and missing-case counts, cost change, p90 change, and measured check spend. It never contains prompts, inputs, approved answers, images, outputs, judge reasons, scorer identity or code, provider keys, the destination URL, or the signing secret.

Signed route-health alert
export EVALT_WEBHOOK_SECRET="replace-with-a-long-random-secret"

python3 -m evalt monitor @support-baseline-v1 \
  --route support-routing \
  --max-cost-usd 0.10 \
  --webhook-url https://alerts.example.com/evalt \
  --webhook-destination-id incident-pipeline \
  --webhook-secret-env EVALT_WEBHOOK_SECRET \
  --webhook-required \
  --webhook-audit .evalt/webhook-deliveries.jsonl
X-Evalt-Signaturesha256=HMAC(secret, timestamp + "." + exact_body).
Idempotency-KeyStable evt_… identity across retries and explicit replay.
route.health.degradedThe prior audited state was healthy and the new frozen check regressed.
route.health.recoveredThe prior audited state regressed and the new frozen check is healthy.
ci.gate.pass|fail|errorOne aggregate GitHub Action decision.

Delivery uses HTTPS only, rejects credentials and fragments in the URL, follows no redirects, resolves and pins a public address for each attempt, and denies private, loopback, link-local, reserved, multicast, and unspecified destinations by default. It retries only bounded transport failures and selected retryable HTTP statuses, honors a capped numeric Retry-After, and retains no response body.

The local JSONL audit contains the exact aggregate event, status codes, retry timing, opaque destination label, and delivery result. Replay preserves the original event and idempotency identity:

Replay one failed event
python3 -m evalt webhook replay evt_0123456789abcdef0123456789abcdef \
  --webhook-url https://alerts.example.com/evalt \
  --webhook-destination-id incident-pipeline \
  --webhook-secret-env EVALT_WEBHOOK_SECRET \
  --webhook-audit .evalt/webhook-deliveries.jsonl
Secrets stay outside Evalt artifactsFor local use, load the secret from an environment variable. For the Evalt Action, pass webhook-secret: ${{ secrets.EVALT_WEBHOOK_SECRET }}. The dashboard accepts only the URL, opaque label, audit path, and secret variable name needed to generate a local command; it never accepts the secret value.

The private-network override exists for explicitly trusted self-hosted environments. Enabling it weakens the default SSRF boundary. Route names are also omitted by default; add --webhook-include-route-name only when the destination is authorized to receive that label.

Regression gate

Make measured quality a deploy check

Shell
python3 -m evalt check candidate.json \
  --baseline baseline.json \
  --min-pass-rate 0.95 \
  --max-cost-per-success 0.002 \
  --max-regressions 0 \
  --max-quality-drop-pp 0 \
  --require-complete-coverage
0The result clears every requested gate.
1The measured result is valid but fails an absolute or frozen-baseline gate.
2The input is invalid or the check could not run.

python3 -m evalt check is offline. With --baseline, it first requires identical non-empty suite hashes, then rejects missing cases, newly failing frozen cases, or aggregate quality loss. Optional --max-cost-increase-pct and --max-p90-increase-ms limits add measured efficiency guardrails. It does not call OpenRouter or mutate either result.

Portable reports and run comparison

Render saved results or compare two frozen runs without provider access. A comparison is promotion-safe only when both results pin the same suite hash.

Shell
evalt report evalt-result.json \
  --html evalt-report.html \
  --junit evalt-report.xml

evalt compare baseline.json candidate.json \
  --output comparison.json \
  --html comparison.html

evalt compare is the detailed local diagnosis; evalt check --baseline is the deployment decision. Both use the same case-level comparison and refuse promotion when suite hashes differ or are absent.

Provider boundary

Know what leaves your machine

Sent to the selected provider

The prompt package and the specific inputs needed for a production or evaluation request.

Kept local by the SDK

API key, route database, approved/corrected evidence, suite files, result files, and promotion history.

  • Every OpenRouter request asks for Zero Data Retention and denies provider data collection.
  • Endpoint eligibility still depends on the current provider route; Evalt fails closed when the required privacy route is unavailable.
  • Current pricing is refreshed at least hourly by default. An unpriced or unbounded route is ineligible.
  • The open-source SDK adds no platform fee. Provider charges remain visible in the result.
Versioning

Stable contracts, explicit deprecations

Evalt follows semantic versioning for the evalt Python API, CLI command names, suite schema, result schema, and exit-code contract. Deprecations remain for at least one minor release and are documented before removal.

The historical modelsieve, last_good_prompt, and lgp names are compatibility shims. New integrations should use the evalt import and command.

Troubleshooting

Common failure states

No eligible model route

Confirm the provider exposes a current ZDR endpoint for at least one candidate and that its price, context, output, and required parameters fit the suite. Evalt does not silently downgrade the data contract.

Optimization stops before every candidate runs

The hard budget or provider-response deadline was reached. Inspect the partial-coverage result, then raise the explicit test budget, raise the task-appropriate request_timeout_seconds, or narrow the candidate set. Partial coverage is never labeled globally best.

A response is empty or truncated

Evalt starts with generous reasoning-aware completion headroom: 32,768 tokens without reasoning, 65,536 at low, 98,304 at medium, and up to 131,072 at high, clamped to the route's natural output limit and remaining context. An empty or explicitly truncated response is retried only when a genuinely larger valid ceiling exists. A second cutoff remains an honest unavailable configuration rather than an infinite paid retry loop.

Too few final-test cases

With the default 20% final-test split, provide at least 25 distinct approved scenarios to obtain five final-test scenarios. Repeats measure consistency but do not inflate the distinct case count.

Need a reproducible issue report

Run offline validation, preserve the sanitized suite and result JSON, include evalt --version, and open a GitHub issue without API keys or private production inputs.

Ready to test a real repeated task?

Start with the route. Keep the evidence.

Open the Evalt workspaceInstall from PyPI