A Jev-style /v1/systemone API in front of any OpenAI-compatible LLM server.
TypeSafe AI's Jev is a "System One" model: you send it a
piece of state plus typed questions, and it returns typed answers with
probabilities — no generated prose. Jev itself is a closed, hosted model.
jev-bridge reproduces its API surface and decision semantics on top of
any OpenAI-compatible backend (SGLang, vLLM, llama.cpp, LMDeploy, TGI, …) by
reading the model's next-token distribution for the answer labels instead of
generating JSON.
client ──POST /v1/systemone──▶ jev-bridge ──POST /chat/completions──▶ SGLang / vLLM / llama.cpp
(Jev wire format) (max_tokens=1, logprobs)
Classification with an instruct LLM is usually done by prompting for JSON and
praying. jev-bridge instead:
- Puts the state in the first user turn (the system message is one fixed string with no state in it) and each question in the same turn after the state, so all questions of one request share an identical byte prefix — backends with prefix caching (SGLang radix cache, vLLM APC) reuse the state prefill. See Prompt caching for what is measured.
- Appends the answer options as single-token labels (
A/B/C…,true/false,0/1/2…) and asks for exactly one label. - Requests
max_tokens=1withlogprobs+top_logprobsand computes a restricted softmax over the labels — the probabilities sum to 1 over the declared options, like Jev's answers do. - Derives
choice/score/nouland aconfidencevalue per answer.
No fine-tuning, no calibration training, no output-token decoding.
pip install git+https://github.com/TOSUKUi/jev-bridge.git # or: pip install -e ".[dev]" from a checkout
export JEVB_BACKEND_BASE_URL="http://127.0.0.1:30010/v1" # any OpenAI-compatible server
export JEVB_BACKEND_MODEL="qwen3.8-flash-next"
jev-bridge serve --host 0.0.0.0 --port 8900No server of your own? Point it at the official OpenAI API instead (add
JEVB_BACKEND_API_KEY, JEVB_DISABLE_THINKING=0, JEVB_PREFILL_ASSISTANT=0) —
Docker works through OpenAI / llama.cpp / vLLM.
Ask a question:
curl -s http://127.0.0.1:8900/v1/systemone -H 'Content-Type: application/json' -d '{
"model": "jev-latest",
"state": "My card was charged twice for order A-104 and I need this fixed today.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Charges, invoices, payment problems",
"shipping": "Delivery status, delays, lost packages",
"returns": "Exchanges, refunds, damaged items"
}
},
"is_urgent": {"type": "noul", "instructions": "Does this message express urgency?"},
"severity": {
"type": "score",
"instructions": "How severe is the issue?",
"criteria": ["Cosmetic", "Workaround exists", "Blocking, no workaround"]
}
}
}'Response:
{
"model": "jev-latest",
"answers": {
"department": {
"type": "choice",
"choice": "billing",
"confidence": 0.9813,
"probabilities": {"billing": 0.9934, "shipping": 0.0033, "returns": 0.0033}
},
"is_urgent": {"type": "noul", "noul": 0.9987},
"severity": {
"type": "score",
"score": 2.0,
"confidence": 0.9971,
"probabilities": {"0": 0.0008, "1": 0.0014, "2": 0.9978},
"legend": {"0": "Cosmetic", "1": "Workaround exists", "2": "Blocking, no workaround"}
}
},
"usage": {"prompt_tokens": 1234, "completion_tokens": 3, "questions": 3, "elapsed_ms": 187}
}
choiceprobabilities are keyed by the option names fromcriteria, andscoreprobabilities by level index ("0","1", …), matching Jev.noulhas no separate confidence field — its probability is the answer.
POST /v1/systemone
| field | type | notes |
|---|---|---|
model |
string, optional | echoed back; the backend model is chosen by JEVB_BACKEND_MODEL |
state |
string or JSON | anything the model should judge |
questions |
object | map of question name → question |
Question types (matching Jev's primitives):
| type | fields | answer |
|---|---|---|
choice |
instructions, criteria = map of option → description (≤ 255) |
choice, confidence, probabilities |
noul |
instructions, optional criteria = {true, false} descriptions |
noul = P(true) |
score |
instructions, criteria = ordered list of 2–10 levels (lowest first) |
score = Σ i·pᵢ (may fall between rungs), confidence, probabilities, legend |
| env | default | meaning |
|---|---|---|
JEVB_BACKEND_BASE_URL |
— (required) | OpenAI-compatible base URL, e.g. http://host:8000/v1 |
JEVB_BACKEND_MODEL |
default |
model name sent to the backend |
JEVB_BACKEND_API_KEY |
dummy |
bearer token for the backend |
JEVB_BACKEND_TIMEOUT |
60 |
per-call timeout (seconds) |
JEVB_MAX_CONCURRENCY |
8 |
in-flight question calls per request |
JEVB_TOP_K |
20 |
minimum top_logprobs requested (raised to cover all options) |
JEVB_CONFIDENCE_METHOD |
linear |
linear | max_prob | entropy |
JEVB_PREFILL_ASSISTANT |
1 |
append <think></think> to suppress thinking-model preamble |
JEVB_DISABLE_THINKING |
1 |
send chat_template_kwargs: {enable_thinking: false, preserve_thinking: false}; auto-retries without it if the backend rejects it |
JEVB_BACKEND_EXTRA_BODY |
— | JSON merged into each chat-completions body, e.g. {"chat_template_kwargs":{"enable_thinking":false}}. Naming max_completion_tokens here also selects the field that carries the token budget |
JEVB_MODEL_NAME |
jev-bridge-1 |
reported model name when the request has no model |
JEVB_ALLOW_LOCAL_IMAGES |
0 |
allow local file paths in images (off = data URI / URL / base64 only) |
JEVB_MAX_IMAGE_BYTES |
20971520 (20 MiB) |
per-image size cap after decode |
JEVB_HOST / JEVB_PORT |
0.0.0.0 / 8900 |
listen address for jev-bridge serve |
linear is the default: (K·p_max − 1)/(K − 1). It maps a uniform
distribution to 0 and a one-hot distribution to 1, and it matches the
confidence values observable in Jev's published examples (e.g. {0.0, 0.7, 0.3} → ≈0.54–0.55). max_prob and entropy (1 − H/lnK) are also available.
Confidence here is a distribution-shape statistic, not Jev's RLCD-calibrated confidence. Treat thresholds as drift-prone and validate them on your task.
The prompt is laid out for prefix caching. Every question of a request sends the same bytes up to the question block:
[system] fixed instruction text, no state
[user] [images…] + "STATE:\n" + <serialized state> + "\n\n" + <question block>
[assistant] "<|im_start|></think>" (JEVB_PREFILL_ASSISTANT)
The serialized state is produced once per request, there are no ids, timestamps
or seeds in the body, and httpx serialises with a fixed key order — so the same
input produces the same bytes, and tests/test_api.py asserts that the N
per-question calls share one prefix. One consequence worth knowing:
serialize_state re-dumps a dict state with indent=2 and wire key order,
so a client that sends the same state with reordered keys gets a different
prefix (and a different prompt).
Measured on the Qwen3.8-Flash-Next endpoint described in Performance, one token out:
| prompt | cold | warm |
|---|---|---|
| ~110 tokens (typical template) | 106 ms | 106 ms |
| ~1.4k tokens of state | 190 ms | 135 ms |
| ~2.8k tokens of state | 293 ms | — |
So a cached prefix is close to free (~+30 ms for 2k tokens) while a cold prefill
costs roughly +40 ms per extra 1k tokens. Concurrency is what makes the warm path
worth having: four questions over a warm prefix took 215 ms concurrent vs
468 ms sequential, so keep the fan-out once the state is warm (and the warm
hit is served from a brand-new connection pool too — 225 ms — so it is not pinned
to a connection). The cold case is the one to know about: the same four questions
over the same cold 2.7k-token prefix took 665 ms sequential (per request
114 / 117 / 119 / 287 — one prefill, three hits) and 880 ms concurrent. That
concurrent figure is consistent with the burst being admitted before any prefill
is committable, i.e. several prefills of the same prefix. Note the epistemic
status: this is inferred from wall-clock. The server was not started with
--enable-cache-report, so usage carries no cached_tokens and no measurement
here observes the cache directly.
The one lever is JEVB_MAX_CONCURRENCY=1 — every question sequential, so each one
hits the prefix the previous one warmed. Good for long cold states, bad for warm
throughput (468 ms vs 215 ms above).
A first-question-then-fan-out primer was implemented, measured, and removed. On a synthetic prompt (2.7k-token state, one-line questions) it won — 501 ms including the primer vs 841 ms unprimed — but on the prompts this bridge actually sends it lost at every size tested: +79 ms at 4 questions, +92 ms at 6, and +92 ms replaying the captured bodies (699 → 791 ms). The primer is one of the N calls doing the same prefill, so it saves at most (N−1)/N of that work and costs a full sequential round trip up front; with the bridge's question blocks being a few hundred tokens rather than one line, that trade is negative. Fan-out is unconditional.
If you want to warm a prefix yourself, the cheap call is max_tokens: 0 — SGLang
runs it prefill-only (completion_tokens: 0, no decode step scheduled), llama.cpp
documents the same as n_predict: 0. The cost of such a call here was 234–448 ms
cold for a 4.1k-token prefix and 117 ms once warm. Whether it buys you anything
depends on what follows it: on the 2.7k-token single-line-question prompt the
following 4-question burst went 841 → 217 ms, while on the two-tier shape (4.1k
judgment JSON + a ~500-token varying context) the burst went 849 → 490 ms but the
primer's own 448 ms made the combined 938 ms worse than not warming at all.
Warm ahead of the request, when a spec changes or on idle, and treat it as a GPU
expense rather than a free one — it competes with live traffic. Two traps: it must
be spelled max_tokens (see the SGLang note below), and cached_tokens stays
invisible in usage unless the server was started with --enable-cache-report —
a missing field is not a cache miss.
The match is a plain forward match on the rendered token sequence from token 0
— not per message. Reuse length is the longest common prefix with something
already cached, cut off at a block/page boundary (vLLM caches full blocks only,
SGLang inserts at page_size), and a block is only insertable once its prefill
has run. Consequences, measured on this endpoint with a ~1.9k-token seed:
| prompt | ms | prompt tokens |
|---|---|---|
| seed, first touch | 233 | 1941 |
| same prefix, 0 appended | 126 | 1941 |
| same prefix, +910 tokens appended | 152 | 2851 |
| same prefix, +1860 tokens appended | 216 | 3801 |
| same prefix, only the question changed | 128–131 | 1946 |
| one nonce inserted at the front of the state | 214–219 | 1945 |
Appending is nearly free (you pay the new tail); moving the divergence point costs everything after it. The nonce case re-prefilled the whole prompt because the divergence sat at the start of the user turn and the shared system block is only a few dozen tokens — a change further down would have kept everything above it. Watch for nonces, timestamps, turn counters and reordered JSON keys. Eviction is documented to drop the deepest tail first (SGLang evicts radix leaves, vLLM frees in reverse order), which is why the long context is what you tend to lose under memory pressure; we did not measure retention here.
If the stable part of your prompt is the question spec and the state is what
changes (a game loop, say), pass one string as state with the spec first:
state = "<judgment JSON>\n\n<current context>". serialize_state returns a
string untouched, so the cached prefix becomes system+spec, and the spec half is
reused across every request while only the context tail re-prefills — measured on
a 4.1k-token spec: first request 519 ms, same spec with a fresh context 173 ms,
and a max_tokens: 0 primer on the spec alone takes a 4-question concurrent burst
from 849 ms to 490 ms.
Treat that as a prompt change, not a config flag: it needs its own scoring
regression before you ship it. The spec lands inside the untrusted STATE region
(the only separator is a newline, and nothing enforces precedence between your
instructions and the payload); a spec duplicated into state gives you two
sources of truth that can disagree while the wire format still looks Jev-valid;
images are rendered before the state, so the claim is void the moment you attach
one; and a spec long enough to disturb the answer still returns HTTP 200 with tidy
probabilities, because labels absent from the top-K get a floor and are
normalised. Measure accuracy on labelled cases before and after, not just latency.
What the bridge does not send: prompt_cache_key, a session id, or any
custom header (there is no header configuration; a constant body field can be
injected with JEVB_BACKEND_EXTRA_BODY). With OpenAI that is usually moot —
prompt caching is automatic for prompts of 1024 tokens or more
(usage.prompt_tokens_details.cached_tokens reports the hit, eviction after ~5–10
min idle), and a typical jev-bridge template is ~110–200 tokens, i.e. below the
threshold and never cached. OpenAI's own guidance to set prompt_cache_key is
about pinning requests that share a prefix to the same cache — relevant as soon
as a gateway fans one state's questions out over several replicas.
The principle: the score comes from the first sampled token. Anything the
model emits before the answer label costs the request its signal, so reasoning
has to be switched off at the source. jev-bridge does this with two layers,
on by default:
chat_template_kwargs: {"enable_thinking": false, "preserve_thinking": false}makes the backend's own chat template open the assistant turn with an empty<|im_start|>\n\n</think>\n\nblock — this is the switch that actually works on Qwen3-style templates (SGLang, vLLM, current llama.cpp). If the backend rejects the field (HTTP 400/422) the bridge automatically retries without it and remembers that; if it silently ignores it, no retry fires and you get the flat answers described below.- Assistant prefill
<|im_start|>assistant<|im_start|>\n\n</think>\n\nis appended to the messages. This only helps where the server honours continuation prefill (llama.cpp does) — measured through a LiteLLM proxy, an endpoint that reads the assistant turn as plain context gained nothing from it.
Backends that speak reasoning_effort instead of template kwargs take it through
JEVB_BACKEND_EXTRA_BODY:
export JEVB_BACKEND_EXTRA_BODY='{"reasoning_effort":"none"}'Two measured caveats: an effort level is not an off-switch (low still opened
with reasoning text on a Qwen3 reasoner), and a value the server does not accept
comes back as HTTP 400 surfaced as 502 — the backend's own message names the
values it accepts.
What "reasoning is not off" looks like in the answers: every option lands on the same probability and confidence collapses —
choice → probabilities {"a": 0.3333, "b": 0.3333, "c": 0.3333}, confidence 0.0 noul → 0.5
A flat distribution on a model you expect to be confident means this, not a bad
model. Behind a proxy, verify it on the wire: logprobs coming back does not
prove chat_template_kwargs arrived (and LiteLLM with drop_params: true drops
logprobs while still answering 200).
Tune with JEVB_DISABLE_THINKING=0 and/or JEVB_PREFILL_ASSISTANT=0. To pass
custom template kwargs instead, use JEVB_BACKEND_EXTRA_BODY — it takes
precedence over the built-in defaults.
docker-compose.yml at the repo root is the default deployment: the official
OpenAI API.
export OPENAI_API_KEY=sk-...
docker compose up
curl -s http://127.0.0.1:8900/healthThe compose file builds the image from this checkout (build: . tagged
jev-bridge:latest) — replace those two lines with a published image once you
have one. Switching backend is only ever a matter of
JEVB_BACKEND_BASE_URL + JEVB_BACKEND_MODEL (+ JEVB_BACKEND_API_KEY if the
server checks a token) — by editing the compose file, dropping a
docker-compose.override.yml next to it, or using the docker run form below.
The three examples below are the same bridge and the same questions; what
differs is what each backend wants to be told.
This is exactly what the compose file ships. chat_template_kwargs is a
vLLM/SGLang extension and the Chat API has no assistant prefill, so both
thinking-suppression layers are switched off; the model must support logprobs,
and top_logprobs is capped at 20 — which is already the JEVB_TOP_K
default, so there is nothing to raise. Equivalent one-liner:
docker run --rm -p 8900:8900 \
-e JEVB_BACKEND_BASE_URL=https://api.openai.com/v1 \
-e JEVB_BACKEND_MODEL=gpt-4o-mini \
-e JEVB_BACKEND_API_KEY="$OPENAI_API_KEY" \
-e JEVB_DISABLE_THINKING=0 \
-e JEVB_PREFILL_ASSISTANT=0 \
jev-bridgeScoring needs the first sampled token's logprobs, and it needs more than one candidate to compare. Measured against the Chat Completions API:
| models | verdict |
|---|---|
gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano |
work as shipped; top_logprobs honoured up to 20 |
gpt-5.4-mini, gpt-5.4-nano, gpt-5.6-luna, gpt-5.6-terra, gpt-5.6-sol |
work with the recipe below (reasoning family) |
o4-mini, gpt-5-nano |
unusable: You are not allowed to request logprobs from this model |
The GPT-5.x reasoning models need three things that are model policy, not bridge settings:
export JEVB_BACKEND_EXTRA_BODY='{"reasoning_effort":"none","max_completion_tokens":8}'
export JEVB_TOP_K=5logprobsis refused on a reasoning model until reasoning is off:Unsupported parameter: 'logprobs' is not supported with this model.reasoning_effort: "none"unlocks it; a level does not (lowstill refuses).max_tokensis rejected in favour ofmax_completion_tokens— naming the latter inJEVB_BACKEND_EXTRA_BODYmakes the first request valid (a backend that only tells you at runtime is handled by an automatic retry). A budget of 1 token is refused outright (Could not finish the message); 4 or more works.top_logprobsis capped at 5, so keepJEVB_TOP_K=5— that is 5 options max.
The catch: the candidate list comes back truncated. OpenAI returns only the
tokens carrying real probability mass, so a confident answer arrives as a single
entry ({"A": -0.0}) and every other label falls to the bridge's floor value:
{"billing": 0.9993, "shipping": 0.0003, "returns": 0.0003} — the same numbers
however the model leaned. gpt-4o-mini on the same ambiguous state returns
{"billing": 0.148, "shipping": 0.0, "returns": 0.852}. It is truncation, not a
one-token policy (a coin-flip prompt returns two entries, -0.34 / -1.25), but
the consequence stands: on the 5.x models the argmax is trustworthy while
probabilities, confidence, thresholds and default_when are endpoints rather
than measurements.
Switching to the Responses API buys nothing here: the refusal is a model
policy and reads the same there (logprobs are not supported with reasoning models). The bridge speaks chat/completions, which is also what llama.cpp,
vLLM and SGLang speak.
Nothing to configure. Current builds read chat_template_kwargs and map
enable_thinking onto the chat template; older builds ignore the field, and the
assistant prefill (on by default) covers those. top_logprobs is translated to
llama.cpp's n_probs (default 20) with no OpenAI-style cap, but a large top-K
per token costs bandwidth — raise JEVB_TOP_K only when a question really needs
it. Give the server a slot per concurrent question (--parallel ≥
JEVB_MAX_CONCURRENCY).
llama-server -m Qwen3-8B-Instruct-Q4_K_M.gguf -c 16384 --port 8080 --parallel 8
docker run --rm -p 8900:8900 \
-e JEVB_BACKEND_BASE_URL=http://host.docker.internal:8080/v1 \
-e JEVB_BACKEND_MODEL=qwen3-8b-instruct \
--add-host=host.docker.internal:host-gateway \
jev-bridgechat_template_kwargs is understood, so thinking models are handled by the
defaults. --max-logprobs caps top_logprobs (default 20, the same as the
bridge's JEVB_TOP_K) — raise it when you need more than 20 options, and keep
prefix caching on so the shared state is prefilled once.
vllm serve Qwen/Qwen3-8B --max-logprobs 64 --enable-prefix-caching
docker run --rm -p 8900:8900 \
-e JEVB_BACKEND_BASE_URL=http://host.docker.internal:8000/v1 \
-e JEVB_BACKEND_MODEL=Qwen/Qwen3-8B \
-e JEVB_TOP_K=64 \
--add-host=host.docker.internal:host-gateway \
jev-bridgeThe same shape covers SGLang (python -m sglang.launch_server --model … --enable-cache-report) — that is the configuration the
Performance numbers were measured on, behind a LiteLLM gateway.
Two SGLang specifics worth knowing:
max_tokens: 0is a genuine prefill-only request (SGLang'sis_prefill_onlypath schedules no decode step, and the response carriescompletion_tokens: 0), and it warms the radix cache.max_completion_tokens: 0is not the same request: the field is read asmax_completion_tokens or max_tokens, so the 0 vanishes and the budget becomes1 << 30— measured, it generated 292 tokens. When you mean zero, sendmax_tokens. (Speculative decoding disables the prefill-only fast path.)usage.prompt_tokens_details.cached_tokensappears only with--enable-cache-report; otherwise a cache hit leaves no trace inusage.
jev-bridge serve [--host H] [--port P] # run the proxy
jev-bridge probe http://host:8000/v1 # score 3 sample questions directly against a backend
jev-bridge probe http://localhost:8900/v1/systemone # end-to-end check of a running bridgeJev itself is text-only, so images is a jev-bridge extension. Add a
top-level images array next to state; images and state text share the
same user turn (templates reject images in system messages), and the question
stays at the end so prefix caching still reuses the image prefill:
{
"state": "What is shown in this photo?",
"images": ["data:image/png;base64,iVBOR…"],
"questions": {
"scene": {"type": "choice", "instructions": "Where is this?", "criteria": {"indoor": "…", "outdoor": "…"}},
"has_people": {"type": "noul", "instructions": "Are people visible?"}
}
}Accepted image references:
| form | example |
|---|---|
| data URI | data:image/png;base64,iVBOR… |
| URL | https://example.com/photo.jpg (backend fetches it) |
| bare base64 | iVBORw0KGgo… (format sniffed from magic bytes) |
| local path | /path/to.png — only with JEVB_ALLOW_LOCAL_IMAGES=1 |
Notes:
- The format is re-sniffed from the payload; a mislabeled data URI is corrected, and a data URI whose bytes are not a recognized image is rejected (400).
- Native formats: PNG, JPEG, GIF, WEBP, BMP. Other formats must be converted
by the caller; per-image decode cap
JEVB_MAX_IMAGE_BYTES(default 20 MiB). - The backend model must be vision-capable — on a text-only model the backend rejects the request and the bridge returns 502 with the backend's message.
- Image tokens are part of the shared prefix, so N questions about one image still prefill the image once.
Measured end to end, warm, on a single RTX PRO 6000 serving Qwen3.8-Flash-Next with SGLang, through the bridge, 10 runs each:
direct SGLang via LiteLLM gateway
1 question (noul) 81 ms 91 ms
3 questions 158 ms 169 ms
6 questions 320 ms 340 ms
The second column is the same SGLang server reached through a LiteLLM gateway
(chat_template_kwargs is honoured on both routes, so thinking is off). The hop
is worth ~10 ms at one question and ~20 ms at six — real, but not where the time
goes. These figures replace an earlier 111 / 201 / 366 ms set taken on another
day; treat single-digit percentages as day-to-day noise and re-measure on your own
hardware.
At temperature: 0 the endpoint is not bit-reproducible on this machine. The
same request body, twelve times in a row:
| questions per request | p(the top option) over 12 runs | peak-to-peak |
|---|---|---|
| 1 | 0.667 … 0.738 | 0.071 |
| 6 | 0.6985 every time | 0.000 |
Greedy decoding fixes the sampling, not the arithmetic: continuous batching changes the batch a sequence is folded into, and reduction order moves logits by just enough to matter near a tie. Do not treat the fourth decimal as a measurement, and give thresholds real margins. (The narrow reading of the table: under this shared load, a six-question request came back identical across twelve runs while a single-question one drifted. It is not evidence that concurrency improves determinism, and the 6-question row is six independent calls sharing a prefix — not one joint prompt.)
Four measured dead ends, so you don't spend a week on them:
- Per-question cost. ~47 ms per extra question when fired back to back, ~63 ms when fired with 3 s gaps between requests — i.e. leaving it idle makes the slope worse, so this is not burst queueing. Attribution is unmeasured (no per-request scheduler or GPU instrumentation from the client side).
- Compact state.
json.dumps(..., separators=(",", ":"))instead ofindent=2cuts the prompt 2789 → 1707 tokens/call (-39%) and cold latency 965 → 839 ms (paired,/flush_cachebefore every shot). It did not win warm (one run said +3 ms, another -58 ms) and it moved probabilities by up to 0.27 and confidence by 0.41 on the same six questions with zero argmax changes. Whitespace is model input: pass a pre-serialized compact string if you want the tokens, but re-run your own scoring regression first. logit_bias. Honoured, direct and through the gateway alike, with the same returned bytes (/tokenize {"prompt": "B"}→ id 33 on this tokenizer;true/falseare single tokens 1802/3721). But the returned logprobs are conditioned on not being a banned token, renormalised over the rest of the vocabulary — not over your label set — so it can suppress an option, it cannot hand you measured probabilities for a set of labels. It does not replace the floor heuristic.top_logprobsbeyond 20. This backend accepts 32/64/100 (their API is not capped like OpenAI's), yet across 3-option and 10-option probes every label was already inside the top 20, so the floor never fired and raising K moved probabilities by <= 0.11 — inside the run-to-run wobble above. LeaveJEVB_TOP_Kalone unless your labels genuinely fall outside the top 20.
Fan-in — every question in one prompt, read the label at each position — is the one
measured win (6 questions 137 ms vs 337 ms in the same run), and it is not offered: it scores
P(label_i | state, all questions, earlier answers) instead of
P(label_i | state, question_i), which flipped the argmax on 2 of 6 questions and
moved a probability by as much as 0.55. A decision engine whose product is a probability
does not get to redefine it for a latency number.
The questions of one request are issued concurrently (JEVB_MAX_CONCURRENCY,
default 8), and N questions at once is a different quantity from N questions in
a row. Four simultaneous 3-question requests against the same warm endpoint:
wall per-request p50 slowest wall/N
direct SGLang 498 ms 370 ms 491 ms 124 ms
via LiteLLM 526 ms 397 ms 520 ms 132 ms
What a caller feels is the per-request p50 (~370–400 ms), not the 124–132 ms:
wall/N is throughput written as if it were a latency. (An earlier revision of this
README published exactly that amortised figure as "median ms/request", which
understated user-visible latency by roughly 3×; examples/bench.py now prints
wall, p50, slowest and wall/N separately.) 24 backend calls in ~990 ms is ~24
calls/s, while the bridge's own share of a ~85 ms single-question budget stays
around 4 ms. If your gateway puts a rpm or max_parallel_requests cap on the
model group, that cap — not this number — is your ceiling; check it before
planning capacity around 24 calls/s.
Reproduce with python examples/bench.py http://127.0.0.1:8900 20. For
comparison, TypeSafe reports 70–500 ms for hosted Jev and a community
measurement found 3 batched questions at ~212 ms.
Same bridge against the official OpenAI API (JEVB_DISABLE_THINKING=0,
JEVB_PREFILL_ASSISTANT=0, median of 6 warm runs, network included):
gpt-4.1-mini 1q ~415 ms 3q ~560 ms gpt-5.6-luna 1q ~608 ms 3q ~629 ms
gpt-4.1-nano 1q ~438 ms 3q ~523 ms gpt-5.4-mini 1q ~662 ms 3q ~729 ms
gpt-4o-mini 1q ~583 ms 3q ~570 ms gpt-5.6-terra 1q ~813 ms 3q ~926 ms
gpt-4o 1q ~597 ms 3q ~617 ms gpt-5.6-sol 1q ~812 ms 3q ~1077 ms
The 5.x numbers need the model recipe. Two things a
hosted API changes: latency is dominated by the round trip (~0.4 s here, so the
local numbers above are the ones to beat), and at temperature: 0 an
unambiguous state comes back one-hot ({billing: 1.0, shipping: 0.0, returns: 0.0}), which leaves a threshold nothing to act on. Genuinely ambiguous states do
stay graded (0.85 / 0.15 / 0.0), so this is about where the operating point
lands, not about the model being unable to express doubt.
- Answers come from one LLM forward pass; there is no RLCD training. Quality tracks the backend model, and confidence is not calibrated.
- No streaming (Jev has none either) and no image input.
choicesupports up to 255 options, but scoring fidelity degrades beyond the backend'stop_logprobslimit — options missing from the returned top-K receive a floor probability. RaiseJEVB_TOP_Kwhere the backend allows it (vLLM:--max-logprobs, SGLang:top_logprobs_num).- Latency scales with question count only through backend batching: all questions of one request run concurrently, and prefix caching makes the shared state nearly free — except on the first request for a state, where fan-out pays the prefill more than once (measured below).
pip install -e ".[dev]"
pytest -qMIT — see LICENSE. jev-bridge is an independent project and is
not affiliated with TypeSafe AI.