Labeling Engine Configuration
Every key in the labeling_engine block, covering sources and confidence methods, the human pools, crowd spend, budgets, runtime limits, features and what validation refuses.
Everything the labeling engine does is set in one labeling_engine block. An invalid block stops the server at startup and lists every problem at once. To have annotators served the engine's requests, also set assignment_strategy: engine. Setting that strategy without an enabled engine is a validation error.
The block has these sections, each covered below:
assignment_strategy: engine
labeling_engine:
enabled: true
schemas: [sentiment] # default: every scheme a source can label
sources: []
cascade: [laya, gemma] # default: the first source alone
contracts: []
humans: {}
crowd: {}
budgets: {}
runtime: {}
features: {}
redaction: {}
distill: {}Top-level keys
Five keys sit directly under labeling_engine:
| Key | Default | Meaning |
|---|---|---|
enabled | false | Turn the engine on. An enabled engine needs at least one source. |
schemas | all | The annotation schemes the engine labels. Under assignment_strategy: engine, a scheme left out is answered only on the items the engine sends to people for its own schemes, not on every item, and validation warns about it. A scheme no source can label (free text, for one) cannot be in the engine, so under this strategy it is never answered on every item. Use another assignment strategy if it must be. |
sources | [] | The models. See Sources. |
cascade | first source | Source ids in the order they are tried. People come after the last. |
contracts | [] | What the labels must satisfy. See Contracts. |
Sources
A source is a model, or an existing set of labels, that proposes a label and a confidence for each item. Every source has a unique id and a kind, and an unknown key on a source is an error:
| Key | Default | Meaning |
|---|---|---|
id | required | Name used in cascade, the call log and the certificate. |
kind | required | One of llm, vlm, decision, detector, judge, jury, trained, static, fixture, asr, lf, simulator. |
endpoint_type | required for llm, vlm, decision, judge | The AI endpoint, such as vllm, openai, anthropic, gemini, ollama, openrouter, openai_vision, ollama_vision, laya or systemone. detector defaults to yolo. |
model | endpoint default | Model name. |
base_url | none | Server URL for self-hosted or OpenAI-compatible endpoints. |
api_key | none | Never hashed into a version and never exported. ${ENV_VAR} is substituted. |
temperature | 0.0 | 0 to 2. |
max_tokens | 256 | Output token limit per call. |
timeout | endpoint default | Seconds per call. |
prompt | built from the scheme | Prompt template. Changing it makes a new source version. |
confidence | {method: auto} | How the source states confidence. See Confidence methods. |
concurrency | runtime.concurrency | Parallel calls to this source. |
schemas | all engine schemes | Restrict the source to these schemes. |
options | {} | Kind-specific options, listed below. |
judge | {} | judge only. The same options as options, under their own key, and options wins where both set one. |
members | none | jury only. At least two other source ids. |
aggregation | plurality | jury only. Plurality is the one rule. |
field / predictions_file | none | static only. Where the existing labels are. The field holds {scheme: label}, or a bare label when the source labels one scheme. A field of the wrong shape on every item fails the run with a message naming it. |
error_rate, seed | none | fixture only. A synthetic source with a known error rate, for tests. |
revision | none | A model revision to record in the version, for example a checkpoint hash. |
Everything that can change an answer (model, prompt, temperature, options) goes into the source's version hash. A new version makes every contract that depends on the source stale.
An answer that is not a value the scheme can hold, such as a label not in its list, a number field that is not a number, or a list on a one-of-N scheme, is recorded as an error and never as a label, and the run's message counts these errors. An item whose calls fail is asked again on later runs, at most three times per source version, and after that it waits for a changed source (a fixed key or prompt is a new version). A run in which one source fails does not stop the other tiers. The next tier labels every item, and potato label run exits with status 1 and names the failed source.
Confidence methods
The engine's acceptance rules threshold on each prediction's confidence, so how a source states confidence decides how much the engine can accept. confidence.method takes one of five values:
confidence.method | Applies to | What it does |
|---|---|---|
verbalized | llm, vlm, judge | The model states 0-100 in its reply. One call, and the least reliable method. |
k_sample | llm | samples replies (5 by default) at sample_temperature (0.7 by default). The label is the most common answer and the confidence is its share. Costs k calls. |
logprob | llm | Labels are lettered and the model answers with one letter, and the confidence is read from the first token's probabilities. One call, usually the best calibrated. Needs a backend that returns logprobs (vLLM, OpenAI-compatible) and at most 20 labels, and a scheme with more is a validation error. |
native | decision, detector, trained | The model's own probabilities. On an llm source it is a validation error. |
auto | all | native where the source has it, otherwise verbalized. |
Kind-specific options
Five source kinds read options of their own:
decisioncovers Laya in-process, or any/v1/systemoneserver such as hosted Jev or OpenJev. One forward pass returns a probability for every option, and it works onradio,select,likertandsliderschemes.endpoint_typemust belayaorsystemone, andsystemoneneedsbase_url, the server root (a URL ending in/v1/systemoneis accepted too). The state is sent as{"text": ...}in both cases, so one checkpoint gives the same answers in-process and behindlaya-serve, andoptions: {state_key: ""}sends the bare string instead. On rating scales (likert,slider), Laya's scores can barely follow the scale, so check a few items before relying on it. The in-processlayaendpoint needspip install 'potato-annotation[decision]'.detectorreads boxes from an object detector onimage_annotationschemes.keep_threshold(0.35) sets the score at or above which a box is part of the prediction. Boxes betweencandidate_threshold(0.05) andkeep_thresholdlower the image's confidence.label_mapmaps detector classes to scheme labels, and unmapped classes are ignored.image_fieldnames the item field with the image path or URL. Paths resolve as the annotation page resolves them, with/media/x.jpgunder the task'smedia_directoryand a relative path undertask_dir.judgetakesrubric(the scheme's description by default),fields(the item fields shown to the judge, in order) andswap_order, which asks twice with the label order reversed and turns a verdict that flips into an abstention. On agent-trace schemes (process_reward,trajectory_eval, ...) the judge labels each step.vlmuses a vision endpoint (openai_vision,anthropic_vision,ollama_vision). For a self-hosted model behind an OpenAI-compatible server, useopenai_visionwithbase_url.trainedtakesartifact_dir,trainer(sklearn-textby default) andrun_id. Distillation usually creates this source, rather than a hand-written config.
humans
The humans block sets who labels and how the engine samples them:
| Key | Default | Meaning |
|---|---|---|
pools | [team] | Who labels: team, crowd, solo. |
calibration_rate | auto | Share of items sampled for blind calibration. auto picks a rate from the contracts' projections, enough labels per class to be testable, at most half of a small study and a fifth of one over 1,000 items. |
dev_fraction | 0.5 | Share of calibration labels used to choose thresholds. The rest test them. |
min_calibration_per_class | 10 | Floor per class before per-class contracts are tested. |
audit_rate | 0.02 | Share of newly accepted machine labels sent back to people, blind, to watch for drift. |
overlap | 0.0 | Share of calibration items labeled by several people. |
overlap_k | 3 | People per overlap item. The no_worse_than_human contract needs 3. |
distribution_k | 5 | People per item for distributional contracts. |
truth | majority | How several people's labels on one item become the label contracts test against, majority or dawid_skene. |
verify_visibility | shown | Whether a verify request shows the machine's answer (shown) or not (blind). |
crowd
The crowd block applies when crowd is in humans.pools, and Prolific is the only crowd platform the engine can drive:
| Key | Default | Meaning |
|---|---|---|
provider | prolific | The crowd platform. |
study_id | none | The Prolific study to resize. |
reward_usd | none | Reward per place, for cost projections. |
per_worker_quota | 20 | Requests per worker. Places are ceil(open requests / quota). |
Crowd recruiting is a dry run by default, and the planner reports what it would do. Spending money needs features.crowd_live_spend: true and a budgets.human_usd cap, and a config that turns on live spend without the cap is rejected. Crowdsourcing covers setting up the Prolific study itself.
budgets
The budgets block stops spending before a limit is exceeded:
| Key | Meaning |
|---|---|
model_usd | Stop calling priced models once this much is spent. Each call reserves its projected cost first. Models with no price, and self-hosted models on localhost, are not limited by it. |
human_usd | Ceiling on crowd spend. Required for live crowd spend. |
human_labels | Maximum human requests the engine may open, counted when each request is opened. |
runtime
The runtime block controls batching, concurrency and how long a person may hold a request:
| Key | Default | Meaning |
|---|---|---|
batch_size | 32 | Items per committed chunk. A killed run resumes at the next chunk without repeating calls. |
concurrency | 4 | Parallel calls per source, overridden by a source's own concurrency. |
cache | true | Reuse replies to identical requests from the same source version. |
retries | 2 | Extra attempts after a timeout, connection error, 429 or 5xx. Every attempt is logged. |
planner_interval | 30 | Seconds between planner passes in the server. |
hold_minutes | 60 | Minutes a person may hold a request without answering before it goes back to the pool. The item also leaves their page, so a tab left open cannot answer it later. |
features
Every feature is off by default except drift:
| Key | What it turns on |
|---|---|
drift | On by default. Acts on audit results. When audits show the accepted labels' error rate is above target, the contract reads violated, machine labels on that scheme are no longer accepted, and fresh calibration labels are drawn. Each certification starts a new audit sequence, so certifying again after a fix restores acceptance. Set it to false to report a breach only, and the contract then reads "met, audits breached". |
distill | Train a model on the study's labels and put it first in the cascade (see distill). |
optimize | Have each planner pass score every subset of the cascade on dev labels and report the cheapest one that keeps the contracts' coverage, at GET /api/engine/v1/schemes/<name>/optimize. Nothing is applied, because adopting a cascade is a config change and a new certification. |
crowd_recruit | Let the planner size the Prolific study, as a dry run unless crowd_live_spend is on. |
crowd_live_spend | Actually change the Prolific study. Needs the crowd pool and budgets.human_usd. |
pii_redaction | Mask emails, URLs, IP addresses, card numbers, SSNs, phone numbers and @handles before text goes to a third-party API. The call log records what was masked as categories and hashes, never the text. |
payload_capture | Keep full prompts and replies under <task_dir>/engine_payloads/. Off by default, and the call log keeps hashes only. |
redaction
redaction.categories lists which of email, url, ip, card, ssn, phone and handle to mask, and defaults to all of them. Masking applies only with features.pii_redaction, and only to third-party APIs. Self-hosted endpoint types (vllm, ollama, laya, yolo, ...) are not masked wherever they run, and neither is any endpoint whose base_url is on the same machine.
distill
With features.distill on, the engine trains a cheap model on the study's labels and puts it first in the cascade:
| Key | Default | Meaning |
|---|---|---|
trainer | sklearn-text | A trainer from potato/training. |
min_labels | 200 | Train only once this many labels exist. |
include_silver | true | Also train on machine labels a contract certified, marked silver. |
every | 2.0 | Retrain when the label count has grown by this factor. |
Items in the cert and audit splits never go into the training set, so the distilled model is never tested on labels it was trained on.
Validation
potato validate config.yaml checks the labeling_engine block along with the rest of the config, and each error names its key, for example labeling_engine.sources[1].kind must be one of ... or labeling_engine.contracts[0].error is required for selective_risk. Validation also refuses:
- a contract key the contract kind does not read, such as a typo of
error; - a contract on a scheme no cascade source can label, with each source's reason;
- a contract on a scheme the engine cannot score (free text, pairwise);
- a
decisionsource whoseendpoint_typeis not a decision model, and anllm,vlmorjudgesource whoseendpoint_typeis one; and no_worse_than_humanwithhumans.overlap_kbelow 3.
Validation also warns when budgets.human_labels is too small to certify a selective_risk contract even if every machine answer were right. Raise the budget, or loosen error, until that warning goes away.
Further reading
- The labeling engine
- Labeling engine contracts
- Command line, REST API, MCP tools and SDK
- AI support, for the endpoint types sources share with the rest of Potato
For implementation details, see the source documentation.