Local proxy that learns your app's typed LLM decisions and answers them with a Laya head. Jev and OpenAI compatible.
See the codestuntd is a local, self-hosted proxy that records the typed decisions your app already makes and
learns to answer them itself. It speaks the Jev System One protocol (POST /v1/systemone, with
choice, score and noul questions) and the OpenAI Chat Completions API, distils each decision
site into a small head on a frozen Laya encoder,
and serves that answer locally with calibrated confidence, handing anything it is unsure about back
to the provider.

Same seed, same typesafe-sdk client, same daemon. Left: the base Laya checkpoint answering
zero-shot. Middle: the BFS oracle that plays teacher. Right: the head stuntd trained from the
teacher's 30 games (138 s), answering 99.6% of the moves itself at 20 ms each. Numbers and
commands are in examples/snake/.
Three ways to run it:
typesafe-sdk at stuntd and the base Laya
checkpoint answers your typed questions.Status: first release, v0.1. Every number below traces to a demo in this repository or to a source listed at the end.
typesafe-sdk client keeps working while you wait.PreToolUse gate answering "may this
command run" in about 50 ms, without sending every command you type to a provider.
examples/devtools/ ships the hook.pip install "stuntd[train]"
stuntd config init
stuntd serve
config init writes a commented stuntd.toml with everything at its default, which means no
OpenAI upstream and no Jev provider. serve says so and loads the base checkpoint:
stuntd listening on http://127.0.0.1:8787
no upstream: only the Jev routes are served
serving jev locally with convaiinnovations/laya
Then change the base URL in your client and nothing else:
from typesafe_sdk import Choice, Noul, TypeSafeClient
QUESTIONS = {
"category": Choice(
instructions="Which part of the product is this ticket about?",
criteria={"billing": "Payments and refunds.", "bug": "Something is broken."},
),
"needs_human": Noul(instructions="Does a support agent have to read this?"),
}
with TypeSafeClient(api_key="local", base_url="http://127.0.0.1:8787") as client:
answer = client.system_one(
state={"subject": "Charged twice", "body": "Two identical charges on Friday."},
questions=QUESTIONS,
)
print(answer.choices["category"].choice, answer.nouls["needs_human"].noul)
Anything that speaks the protocol plugs in the same way, by changing one base URL wherever the
client exposes it: typesafe-sdk, JevRouter, the local-server request of fast-jev-compaction, and
the browser and game agents built on the same SDK.
The key is ignored unless you set [jev] require_key = true. To train a head on this daemon you
need labelled rows: either put the paid Jev API in front of it ([jev] upstream) and let it teach,
or write the rows yourself and stuntd import them.
pip install "stuntd[train]"
stuntd config init
stuntd serve --upstream https://api.openai.com
--upstream is the provider origin only, without /v1: your client's own path is appended to it
unchanged. In the client, point base_url at the daemon:
import os
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key=os.environ["OPENAI_API_KEY"])
Everything keeps working exactly as before. A request whose response_format (or single tool) asks
for an object with one enum, boolean or number field is a typed decision, and stuntd records it
with the provider's answer. Anything else is relayed untouched, streaming included.
Once a site has a few hundred captures:
stuntd status # what has been recorded, per site
stuntd train # one head per site with enough examples
stuntd report # holdout agreement, ECE, the operating point
stuntd enable <site> # let that site answer locally
What a trained head does, measured on a laptop RTX 5060 with convaiinnovations/laya as the base
checkpoint, 3000 generated rows per site and 24 epochs (5890 rows for Snake), with
training.cache_encoder at its default. "Zero-shot" is the same daemon before training, answering
from the base checkpoint alone.
| demo | questions | zero-shot | trained heads | answered locally | p50 per request |
|---|---|---|---|---|---|
| Snake | 1 choice | 0.70 average score | 11.40 average score | 99.6% | 21.1 ms |
| support | choice, score, noul | 69.5 / 34.5 / 66.5% | 99.5 / 85.5 / 97.5% | 90% | 90.3 ms |
| banking | 2 choice | 89.5 / 30.0% | 100.0 / 72.5% | 78% | 75.7 ms |
| devtools | choice, noul | 45.0 / 84.5% | 97.0 / 100.0% | 98% | 58.0 ms |
Every decision site of the three text demos, measured against its teacher on held-out requests before and after the head was trained:
Snake is scored by what the answers do rather than by how many of them are right:
Five of the seven sites in the three text demos reach a holdout agreement of 0.95 or better. The
demos run at the default target_agreement of 0.99, which picks each head's operating point rather
than being a score it reached. The two that fall short, support's urgency at 0.935 and banking's
risk at 0.937, stay in the demos anyway: see limitations.
Latency, size and cost:
| measured | |
|---|---|
| one head answer on CUDA | p50 22 ms, 26 ms in process |
| the same head on CPU | 60 to 62 ms |
| one live answer through the daemon, over TCP | 28 ms |
| what the relay itself adds when the provider answers | 2.1 ms |
| Jev API round trip, through OpenRouter | p50 376 to 389 ms |
| Jev price | $0.042 per 1M input tokens, output free |
| training one head: 5890 rows, 24 epochs | 186 s |
| training three heads: 3000 rows each, 24 epochs | 362 s |
| a trained head on disk | about 50 MB, fp16 |
The first four rows were measured during development on an RTX 5060 laptop running Windows. They come from no published benchmark, and there is no link to give for them. The two training rows are the Snake and support demos of this repository, on the same laptop with the encoder cache on; the devtools demo measured the same 24-epoch run at 730 s with the cache off against 231 s with it on, 3.2x.
Why train at all, rather than run Laya zero-shot. From jevbench, n=500 per dataset, accuracy:
| dataset | labels | Laya zero-shot | Jev | fine-tuned DistilBERT |
|---|---|---|---|---|
| sst2 | 2 | 92.0 | 95.4 | 91.0 |
| agnews | 4 | 90.6 | 84.3 | 91.0 |
| banking77 | 77 | 38.2 | 76.4 | 88.0 |
Zero-shot Laya holds its own at two or four labels and collapses at seventy-seven. A model trained on a few thousand of your own examples does not: that is the bet stuntd makes, and the four demos are what it is measured against.
your app
|
v
+-------------------------------------------------+
| stuntd |
| |
| typed decision? -- no ---------------------------> provider, byte for byte
| | |
| yes |
| v |
| head confident? -- yes --> local answer | 20 to 100 ms, no tokens
| | |
| no, or a 2% check sample |
+---------|----------------------------------------+
v
provider (answer returned to the caller, and recorded as a capture)
A decision site is one place in your code that makes one decision. On the Jev path the site is
the question's name. On the OpenAI path it is a hash of the response schema and the system prompt,
or whatever you put in the X-Stuntd-Site request header.
Each site walks through four modes.
stuntd train fits one classification head per site on the frozen Laya encoder,
holds back 20% of the rows, and reports the holdout agreement, the calibration error and the
confidence threshold at which the head reaches your target_agreement:
category trained holdout agreement 0.992 coverage 1.00 at agreement 0.992 ece 0.024.
Below that threshold the head abstains, which is what coverage measures: the share of requests
it is willing to answer at all. stuntd enable refuses a head whose coverage rounds to zero and
warns below 10%.stuntd status shows the running agreement.stuntd enable <site> and the head answers the caller. Anything under the threshold
still goes to the provider. So does a check_share sample, 2% by default, and those samples are
what keeps the head honest: once agreement over the last window decisions falls below
target_agreement, the site demotes itself back to shadow and logs why. Promotion the other way
is manual unless you set serving.auto_promote.In proxy mode the provider behind [jev] upstream does not have to be the paid API. Any server
that speaks POST /v1/systemone can sit there -- kev, LLM2Jev, the laya-server family -- which
makes it a keyless local teacher: it answers, stuntd records, and the head learns from it.
Nothing is uploaded. Captures live in a SQLite file under your user directory with private permissions, and there is no telemetry.
X-Stuntd headerEvery answer says how it was served, so a silent fallback cannot hide.
On the OpenAI path: the mode, then the fields that apply.
X-Stuntd: live; site=moderation; confidence=0.97
X-Stuntd: collect; site=moderation; reason=low-confidence
X-Stuntd: passthrough; reason=no-schema
| field | values |
|---|---|
| mode | live, shadow, collect, passthrough |
site= | the decision site this request landed on |
confidence= | the head's calibrated confidence, two decimals, on a live answer |
reason= | no-schema, streaming, learning-off, no-upstream, low-confidence, check, retrained, model-error, no-runtime, state-error, upstream-error |
On the Jev path: the mode it served in, then how the questions were answered.
X-Stuntd: jev; mode=local; questions=3; live=2; zeroshot=1
X-Stuntd: jev; mode=proxy; questions=2; live=0; shadow=1; check=0
X-Stuntd: jev; mode=local; questions=1; live=0; zeroshot=1; learn=off
X-Stuntd: jev; mode=local; reason=no-key
| field | values |
|---|---|
mode= | local (no Jev provider behind it) or proxy (the paid API is) |
questions= | how many questions the request asked |
live= | answered by a trained head |
shadow= | site in shadow mode, proxy mode only |
zeroshot= | answered by the base checkpoint, local mode only |
check= | sampled for a comparison with the provider, proxy mode only |
learn=off | learn = false, so nothing was recorded |
reason= | no-key, bad-request, not-parsed, no-runtime, model-error, upstream-error |
stuntd config init writes the file; stuntd config show prints every setting with where it came
from. Every key is optional and a missing one keeps the default.
| key | default | what it does |
|---|---|---|
upstream | "" | Provider origin the OpenAI path forwards to, no path. Empty: only the Jev routes are served. |
host | 127.0.0.1 | Address the daemon listens on. |
port | 8787 | Port the daemon listens on. |
learn | true | Whether the daemon records anything at all. false is a pure proxy, or a pure local Jev. |
redaction.enabled | true | Built-in email and phone rules run before a capture is written. |
redaction.patterns | [] | Extra regular expressions, each replaced with [redacted]. |
storage.db_path | captures.sqlite | Capture database; a relative path resolves against the data directory. |
storage.max_rows | 100000 | Captures kept before the oldest are pruned. |
storage.max_age_days | 30 | Age at which a capture is pruned. |
storage.models_dir | models | Where trained heads live. |
training.min_examples | 300 | Captures a site needs before it is worth training. |
training.holdout | 0.2 | Share of the rows held back to measure the head. |
training.target_agreement | 0.99 | Agreement with the teacher the head must reach; it sets the confidence threshold. |
training.base_model | convaiinnovations/laya | Checkpoint the heads are trained on. A local directory works too. |
training.epochs | 3 | Passes over the training rows. The demos use 24. |
training.device | auto | auto, cpu, cuda or mps. |
training.cache_encoder | true | Whether the frozen encoder runs once per example instead of once per epoch; off re-encodes. |
training.cache_max_mb | 4096 | Most memory the cached encoder output may take, in megabytes; a bigger site trains uncached. |
serving.check_share | 0.02 | Share of live requests still sent to the provider to check the head. Inert in local Jev mode. |
serving.window | 100 | Recent decisions a site is judged on. |
serving.min_window | 20 | Decisions needed before that judgement counts. |
serving.auto_promote | false | Whether a shadow site that holds its target goes live on its own. |
serving.auto_promote_after_hours | 24 | Hours in shadow before it may. |
serving.cache_size | 1000 | Answers kept in memory, one LRU cache shared by every site. |
jev.upstream | "" | Origin of the paid Jev provider, no path. Empty: the base Laya answers locally. |
jev.require_key | false | Whether a Jev request must carry a Bearer token. The token itself is not checked. |
jev.model_name | stuntd | Name reported in the model field of a Jev answer. |
Four runnable demos, each with its own README, its own generated data and its own measured table.
examples/snake/ plays 12x12 Snake over the Jev protocol, one choice per
move. Zero-shot Laya scores 0.70 a game; the head scores 11.40 at target_agreement = 0.95,
answering all but 13 of 3050 moves locally at a p50 of 21.1 ms. At the default 0.99 the same head
scores 3.03 and answers 71% of the moves. That is the difference between "let the head play" and "only when
it is sure", in one table.examples/support/ triages a helpdesk ticket with three questions in one
request: category (choice), urgency (score) and needs_human (noul). 90% of the questions
end up answered locally.examples/banking/ asks a banking intent over 12 labels and a payment risk
verdict. 78% local, and intent reaches a holdout agreement of 1.000.examples/devtools/ gates a coding agent's shell command and calls a test
failure flaky. 98% local, and it ships the Claude Code PreToolUse hook that uses it.The teachers in all four are rules, not an LLM. Three demos use a written rule over the generated text; Snake uses a BFS oracle. No Jev API key was available when these numbers were taken, so the zero-shot rows are the base checkpoint, not the paid provider, and the same tables with the real Jev API as the teacher have not been measured yet. What the demos do show is what a head learns from a teacher that is consistent: the ceiling of the method, not of any one provider.
The encoder is frozen, so a head learns text, not arithmetic over fields. This is the single
most important thing to know. In the banking demo the first version of the risk rule compared the
amount, the country and the hour numerically: the head reached 0.42 and covered 1% of the holdout.
The same decision, written as a sentence about the customer's own history, reaches 0.937 at
coverage 0.57. If your decision is really a calculation, write the calculation, not a model.
choice, noul or score on the Jev path. A request that asks for
prose, a summary or code is relayed and never recorded.training.min_examples is 300 and the demos use 3000 rows. A rare
decision, or one whose option list changes every week, is not a fit.urgency, a four-level score. Its levels overlap in wording, so the head
reaches a 0.935 holdout agreement and answers 71% of the tickets at 99.1% agreement, handing the
other 29% to the provider. That is a working site rather than a failure, but it is the shape to
expect when the labels are not cleanly separated by words.score takes 2 to 10 levels and choice takes at most 255 options. Those are stuntd's own
limits, not a quotation from the Jev API. instructions is optional on a question.: becomes ., so a:b and a.b are one site. Two
questions with the same name but different criteria train one head on mixed data; give them
different names.GET /v1/models is answered by stuntd, in the Jev shape, naming jev.model_name. In proxy
mode it is relayed to the Jev provider. Either way an OpenAI client does not get the provider's
model list through stuntd; every other /v1/models... path is relayed as usual.stuntd status reports "agreement": null, and no site is ever demoted.
stuntd report, which measures the head against held-out rows, is the quality signal there. Put
a Jev provider in front and both come back.stuntd train, or the two contend for
the same GPU.serve loads the base checkpoint when [jev] upstream is empty, because local Jev
answers from it. The daemon runs one pass through the model before it accepts requests, so
startup costs seconds and the first request then runs close to the steady-state p50. Lazy
loading is on the roadmap./v1/systemone and scores each option during the prefill, so reordering the options does not
change the answer. stuntd has neither that order independence nor its image inputs; what it has
is a head that answers without the LLM.stuntd train as a
standalone library: you bring the rows, and nothing records them for you.None of them learns from live traffic: recording the provider's own answers, a shadow phase, a
calibrated threshold and a fallback for everything under it is the loop stuntd adds. And every
Jev-compatible server above -- the laya-server family, kev, LLM2Jev -- can sit behind stuntd as
[jev] upstream, which makes any of them a local teacher that needs no key.
Next: support for new formats.
Later:
stuntd train on a schedule.See CONTRIBUTING.md. make check is what CI runs: ruff format --check,
ruff check, mypy, pyright and pytest. The same file for LLM contributors is
AGENTS.md.
Apache-2.0. See LICENSE.
The decision heads are trained on Laya by Convai Innovations, Apache-2.0.
4 commits
Python
99.9%
Local proxy that learns your app's typed LLM decisions and answers them with a Laya head. Jev and OpenAI compatible.
See the codestuntd is a local, self-hosted proxy that records the typed decisions your app already makes and
learns to answer them itself. It speaks the Jev System One protocol (POST /v1/systemone, with
choice, score and noul questions) and the OpenAI Chat Completions API, distils each decision
site into a small head on a frozen Laya encoder,
and serves that answer locally with calibrated confidence, handing anything it is unsure about back
to the provider.

Same seed, same typesafe-sdk client, same daemon. Left: the base Laya checkpoint answering
zero-shot. Middle: the BFS oracle that plays teacher. Right: the head stuntd trained from the
teacher's 30 games (138 s), answering 99.6% of the moves itself at 20 ms each. Numbers and
commands are in examples/snake/.
Three ways to run it:
typesafe-sdk at stuntd and the base Laya
checkpoint answers your typed questions.Status: first release, v0.1. Every number below traces to a demo in this repository or to a source listed at the end.
typesafe-sdk client keeps working while you wait.PreToolUse gate answering "may this
command run" in about 50 ms, without sending every command you type to a provider.
examples/devtools/ ships the hook.pip install "stuntd[train]"
stuntd config init
stuntd serve
config init writes a commented stuntd.toml with everything at its default, which means no
OpenAI upstream and no Jev provider. serve says so and loads the base checkpoint:
stuntd listening on http://127.0.0.1:8787
no upstream: only the Jev routes are served
serving jev locally with convaiinnovations/laya
Then change the base URL in your client and nothing else:
from typesafe_sdk import Choice, Noul, TypeSafeClient
QUESTIONS = {
"category": Choice(
instructions="Which part of the product is this ticket about?",
criteria={"billing": "Payments and refunds.", "bug": "Something is broken."},
),
"needs_human": Noul(instructions="Does a support agent have to read this?"),
}
with TypeSafeClient(api_key="local", base_url="http://127.0.0.1:8787") as client:
answer = client.system_one(
state={"subject": "Charged twice", "body": "Two identical charges on Friday."},
questions=QUESTIONS,
)
print(answer.choices["category"].choice, answer.nouls["needs_human"].noul)
Anything that speaks the protocol plugs in the same way, by changing one base URL wherever the
client exposes it: typesafe-sdk, JevRouter, the local-server request of fast-jev-compaction, and
the browser and game agents built on the same SDK.
The key is ignored unless you set [jev] require_key = true. To train a head on this daemon you
need labelled rows: either put the paid Jev API in front of it ([jev] upstream) and let it teach,
or write the rows yourself and stuntd import them.
pip install "stuntd[train]"
stuntd config init
stuntd serve --upstream https://api.openai.com
--upstream is the provider origin only, without /v1: your client's own path is appended to it
unchanged. In the client, point base_url at the daemon:
import os
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key=os.environ["OPENAI_API_KEY"])
Everything keeps working exactly as before. A request whose response_format (or single tool) asks
for an object with one enum, boolean or number field is a typed decision, and stuntd records it
with the provider's answer. Anything else is relayed untouched, streaming included.
Once a site has a few hundred captures:
stuntd status # what has been recorded, per site
stuntd train # one head per site with enough examples
stuntd report # holdout agreement, ECE, the operating point
stuntd enable <site> # let that site answer locally
What a trained head does, measured on a laptop RTX 5060 with convaiinnovations/laya as the base
checkpoint, 3000 generated rows per site and 24 epochs (5890 rows for Snake), with
training.cache_encoder at its default. "Zero-shot" is the same daemon before training, answering
from the base checkpoint alone.
| demo | questions | zero-shot | trained heads | answered locally | p50 per request |
|---|---|---|---|---|---|
| Snake | 1 choice | 0.70 average score | 11.40 average score | 99.6% | 21.1 ms |
| support | choice, score, noul | 69.5 / 34.5 / 66.5% | 99.5 / 85.5 / 97.5% | 90% | 90.3 ms |
| banking | 2 choice | 89.5 / 30.0% | 100.0 / 72.5% | 78% | 75.7 ms |
| devtools | choice, noul | 45.0 / 84.5% | 97.0 / 100.0% | 98% | 58.0 ms |
Every decision site of the three text demos, measured against its teacher on held-out requests before and after the head was trained:
Snake is scored by what the answers do rather than by how many of them are right:
Five of the seven sites in the three text demos reach a holdout agreement of 0.95 or better. The
demos run at the default target_agreement of 0.99, which picks each head's operating point rather
than being a score it reached. The two that fall short, support's urgency at 0.935 and banking's
risk at 0.937, stay in the demos anyway: see limitations.
Latency, size and cost:
| measured | |
|---|---|
| one head answer on CUDA | p50 22 ms, 26 ms in process |
| the same head on CPU | 60 to 62 ms |
| one live answer through the daemon, over TCP | 28 ms |
| what the relay itself adds when the provider answers | 2.1 ms |
| Jev API round trip, through OpenRouter | p50 376 to 389 ms |
| Jev price | $0.042 per 1M input tokens, output free |
| training one head: 5890 rows, 24 epochs | 186 s |
| training three heads: 3000 rows each, 24 epochs | 362 s |
| a trained head on disk | about 50 MB, fp16 |
The first four rows were measured during development on an RTX 5060 laptop running Windows. They come from no published benchmark, and there is no link to give for them. The two training rows are the Snake and support demos of this repository, on the same laptop with the encoder cache on; the devtools demo measured the same 24-epoch run at 730 s with the cache off against 231 s with it on, 3.2x.
Why train at all, rather than run Laya zero-shot. From jevbench, n=500 per dataset, accuracy:
| dataset | labels | Laya zero-shot | Jev | fine-tuned DistilBERT |
|---|---|---|---|---|
| sst2 | 2 | 92.0 | 95.4 | 91.0 |
| agnews | 4 | 90.6 | 84.3 | 91.0 |
| banking77 | 77 | 38.2 | 76.4 | 88.0 |
Zero-shot Laya holds its own at two or four labels and collapses at seventy-seven. A model trained on a few thousand of your own examples does not: that is the bet stuntd makes, and the four demos are what it is measured against.
your app
|
v
+-------------------------------------------------+
| stuntd |
| |
| typed decision? -- no ---------------------------> provider, byte for byte
| | |
| yes |
| v |
| head confident? -- yes --> local answer | 20 to 100 ms, no tokens
| | |
| no, or a 2% check sample |
+---------|----------------------------------------+
v
provider (answer returned to the caller, and recorded as a capture)
A decision site is one place in your code that makes one decision. On the Jev path the site is
the question's name. On the OpenAI path it is a hash of the response schema and the system prompt,
or whatever you put in the X-Stuntd-Site request header.
Each site walks through four modes.
stuntd train fits one classification head per site on the frozen Laya encoder,
holds back 20% of the rows, and reports the holdout agreement, the calibration error and the
confidence threshold at which the head reaches your target_agreement:
category trained holdout agreement 0.992 coverage 1.00 at agreement 0.992 ece 0.024.
Below that threshold the head abstains, which is what coverage measures: the share of requests
it is willing to answer at all. stuntd enable refuses a head whose coverage rounds to zero and
warns below 10%.stuntd status shows the running agreement.stuntd enable <site> and the head answers the caller. Anything under the threshold
still goes to the provider. So does a check_share sample, 2% by default, and those samples are
what keeps the head honest: once agreement over the last window decisions falls below
target_agreement, the site demotes itself back to shadow and logs why. Promotion the other way
is manual unless you set serving.auto_promote.In proxy mode the provider behind [jev] upstream does not have to be the paid API. Any server
that speaks POST /v1/systemone can sit there -- kev, LLM2Jev, the laya-server family -- which
makes it a keyless local teacher: it answers, stuntd records, and the head learns from it.
Nothing is uploaded. Captures live in a SQLite file under your user directory with private permissions, and there is no telemetry.
X-Stuntd headerEvery answer says how it was served, so a silent fallback cannot hide.
On the OpenAI path: the mode, then the fields that apply.
X-Stuntd: live; site=moderation; confidence=0.97
X-Stuntd: collect; site=moderation; reason=low-confidence
X-Stuntd: passthrough; reason=no-schema
| field | values |
|---|---|
| mode | live, shadow, collect, passthrough |
site= | the decision site this request landed on |
confidence= | the head's calibrated confidence, two decimals, on a live answer |
reason= | no-schema, streaming, learning-off, no-upstream, low-confidence, check, retrained, model-error, no-runtime, state-error, upstream-error |
On the Jev path: the mode it served in, then how the questions were answered.
X-Stuntd: jev; mode=local; questions=3; live=2; zeroshot=1
X-Stuntd: jev; mode=proxy; questions=2; live=0; shadow=1; check=0
X-Stuntd: jev; mode=local; questions=1; live=0; zeroshot=1; learn=off
X-Stuntd: jev; mode=local; reason=no-key
| field | values |
|---|---|
mode= | local (no Jev provider behind it) or proxy (the paid API is) |
questions= | how many questions the request asked |
live= | answered by a trained head |
shadow= | site in shadow mode, proxy mode only |
zeroshot= | answered by the base checkpoint, local mode only |
check= | sampled for a comparison with the provider, proxy mode only |
learn=off | learn = false, so nothing was recorded |
reason= | no-key, bad-request, not-parsed, no-runtime, model-error, upstream-error |
stuntd config init writes the file; stuntd config show prints every setting with where it came
from. Every key is optional and a missing one keeps the default.
| key | default | what it does |
|---|---|---|
upstream | "" | Provider origin the OpenAI path forwards to, no path. Empty: only the Jev routes are served. |
host | 127.0.0.1 | Address the daemon listens on. |
port | 8787 | Port the daemon listens on. |
learn | true | Whether the daemon records anything at all. false is a pure proxy, or a pure local Jev. |
redaction.enabled | true | Built-in email and phone rules run before a capture is written. |
redaction.patterns | [] | Extra regular expressions, each replaced with [redacted]. |
storage.db_path | captures.sqlite | Capture database; a relative path resolves against the data directory. |
storage.max_rows | 100000 | Captures kept before the oldest are pruned. |
storage.max_age_days | 30 | Age at which a capture is pruned. |
storage.models_dir | models | Where trained heads live. |
training.min_examples | 300 | Captures a site needs before it is worth training. |
training.holdout | 0.2 | Share of the rows held back to measure the head. |
training.target_agreement | 0.99 | Agreement with the teacher the head must reach; it sets the confidence threshold. |
training.base_model | convaiinnovations/laya | Checkpoint the heads are trained on. A local directory works too. |
training.epochs | 3 | Passes over the training rows. The demos use 24. |
training.device | auto | auto, cpu, cuda or mps. |
training.cache_encoder | true | Whether the frozen encoder runs once per example instead of once per epoch; off re-encodes. |
training.cache_max_mb | 4096 | Most memory the cached encoder output may take, in megabytes; a bigger site trains uncached. |
serving.check_share | 0.02 | Share of live requests still sent to the provider to check the head. Inert in local Jev mode. |
serving.window | 100 | Recent decisions a site is judged on. |
serving.min_window | 20 | Decisions needed before that judgement counts. |
serving.auto_promote | false | Whether a shadow site that holds its target goes live on its own. |
serving.auto_promote_after_hours | 24 | Hours in shadow before it may. |
serving.cache_size | 1000 | Answers kept in memory, one LRU cache shared by every site. |
jev.upstream | "" | Origin of the paid Jev provider, no path. Empty: the base Laya answers locally. |
jev.require_key | false | Whether a Jev request must carry a Bearer token. The token itself is not checked. |
jev.model_name | stuntd | Name reported in the model field of a Jev answer. |
Four runnable demos, each with its own README, its own generated data and its own measured table.
examples/snake/ plays 12x12 Snake over the Jev protocol, one choice per
move. Zero-shot Laya scores 0.70 a game; the head scores 11.40 at target_agreement = 0.95,
answering all but 13 of 3050 moves locally at a p50 of 21.1 ms. At the default 0.99 the same head
scores 3.03 and answers 71% of the moves. That is the difference between "let the head play" and "only when
it is sure", in one table.examples/support/ triages a helpdesk ticket with three questions in one
request: category (choice), urgency (score) and needs_human (noul). 90% of the questions
end up answered locally.examples/banking/ asks a banking intent over 12 labels and a payment risk
verdict. 78% local, and intent reaches a holdout agreement of 1.000.examples/devtools/ gates a coding agent's shell command and calls a test
failure flaky. 98% local, and it ships the Claude Code PreToolUse hook that uses it.The teachers in all four are rules, not an LLM. Three demos use a written rule over the generated text; Snake uses a BFS oracle. No Jev API key was available when these numbers were taken, so the zero-shot rows are the base checkpoint, not the paid provider, and the same tables with the real Jev API as the teacher have not been measured yet. What the demos do show is what a head learns from a teacher that is consistent: the ceiling of the method, not of any one provider.
The encoder is frozen, so a head learns text, not arithmetic over fields. This is the single
most important thing to know. In the banking demo the first version of the risk rule compared the
amount, the country and the hour numerically: the head reached 0.42 and covered 1% of the holdout.
The same decision, written as a sentence about the customer's own history, reaches 0.937 at
coverage 0.57. If your decision is really a calculation, write the calculation, not a model.
choice, noul or score on the Jev path. A request that asks for
prose, a summary or code is relayed and never recorded.training.min_examples is 300 and the demos use 3000 rows. A rare
decision, or one whose option list changes every week, is not a fit.urgency, a four-level score. Its levels overlap in wording, so the head
reaches a 0.935 holdout agreement and answers 71% of the tickets at 99.1% agreement, handing the
other 29% to the provider. That is a working site rather than a failure, but it is the shape to
expect when the labels are not cleanly separated by words.score takes 2 to 10 levels and choice takes at most 255 options. Those are stuntd's own
limits, not a quotation from the Jev API. instructions is optional on a question.: becomes ., so a:b and a.b are one site. Two
questions with the same name but different criteria train one head on mixed data; give them
different names.GET /v1/models is answered by stuntd, in the Jev shape, naming jev.model_name. In proxy
mode it is relayed to the Jev provider. Either way an OpenAI client does not get the provider's
model list through stuntd; every other /v1/models... path is relayed as usual.stuntd status reports "agreement": null, and no site is ever demoted.
stuntd report, which measures the head against held-out rows, is the quality signal there. Put
a Jev provider in front and both come back.stuntd train, or the two contend for
the same GPU.serve loads the base checkpoint when [jev] upstream is empty, because local Jev
answers from it. The daemon runs one pass through the model before it accepts requests, so
startup costs seconds and the first request then runs close to the steady-state p50. Lazy
loading is on the roadmap./v1/systemone and scores each option during the prefill, so reordering the options does not
change the answer. stuntd has neither that order independence nor its image inputs; what it has
is a head that answers without the LLM.stuntd train as a
standalone library: you bring the rows, and nothing records them for you.None of them learns from live traffic: recording the provider's own answers, a shadow phase, a
calibrated threshold and a fallback for everything under it is the loop stuntd adds. And every
Jev-compatible server above -- the laya-server family, kev, LLM2Jev -- can sit behind stuntd as
[jev] upstream, which makes any of them a local teacher that needs no key.
Next: support for new formats.
Later:
stuntd train on a schedule.See CONTRIBUTING.md. make check is what CI runs: ruff format --check,
ruff check, mypy, pyright and pytest. The same file for LLM contributors is
AGENTS.md.
Apache-2.0. See LICENSE.
The decision heads are trained on Laya by Convai Innovations, Apache-2.0.
4 commits
Python
99.9%