jev is a thin shell over the Python API. It computes nothing that
jevimage.Jev and jevimage.connect() do not already compute, so a number that
looks wrong here is wrong upstream, not in the formatting. The output is plain
ASCII on purpose; it is meant to survive being pasted into an issue or a log.
ask, train, heads, rm) take --url and drive a remote
jev serve instance instead of local weights. Remote mode runs the same code
path, because jevimage.connect(url) exposes the same methods as
jevimage.load() and the command body is written once against both.
Related pages: Install · Quickstart ·
Question types · Encoders ·
Serving · Python API ·
HTTP API
About the transcripts on this page
Every block below is a command that was run, with its real output. The one substitution: the encoder is a deterministic toy encoder (a 16-dim hash-of-the-bytes model registered astoy / toy-calibrated), not SigLIP,
because this machine has no network for model weights. The images are ds/, the twelve
squares examples/make_fixtures.py writes. The toy is not
semantic (it will happily call a blue square “red”), so read the shapes of these
outputs, not the probabilities. Everything that is not a probability (columns, ids, error
text, exit codes, which install a command needs) is exactly what you will see.
Two cosmetic edits: long scratch-directory paths are elided to /tmp/.../ so the
lines fit, and a repeated listing already printed in full above is cut short with
a ... line. Nothing in a transcript is retyped.
The toy appears in jev encoders listings on this page because it was registered by a
sitecustomize.py on PYTHONPATH. That is also the only way to get a custom encoder
into the CLI at all. See Encoders and the CLI.
What each command needs installed
jev encoders listing the registry without torch is deliberate: seeing which
encoders exist should not require the stack that runs them. jev rm gets there
by accident of being an unlink; it never has to read a head file.
The failure is one message, not a traceback, and it names both fixes:
jev serve cannot,
so it gets its own message:
jev heads and jev train produce the same two-part message as jev ask, with
the command name substituted.
Listing local heads needs torch even though a head listing is only metadata: the head files are torch tensors, and the command imports the training module to format them. Listing a server’s heads does not, which is the whole reason that import sits inside the function rather than at the top of the module.
Exit codes
Exit 1 covers
ValueError, KeyError, OSError, ImportError and JevError
(the client’s HTTP error). Anything else is a bug and prints a traceback.
jev serve is the exception: uvicorn installs its own SIGINT handler, shuts the
app down and returns normally, so Ctrl-C there exits 0.
Environment variables
JEV_HOME at work. Note the /heads suffix the CLI appends:
Remote mode: --url and $JEV_URL
Set either and ask, train, heads and rm run against a jev serve
instance: no torch, no weights, no GPU on this machine. --url beats $JEV_URL.
The last line of jev ask output tells you which backend answered, so a
transcript is self-describing:
encoder toy instead, and for this
image the numbers are identical: same engine, same arithmetic, different side of
a socket.
--encoder is silently ignored when a URL is set. The server decides which
encoder runs; nothing is downloaded here. This command answers from the remote
toy encoder and never touches siglip2-giant-384:
jev encoders has no --url: it lists the registry compiled into this install,
which is not necessarily what the server runs. Ask the server itself:
curl $JEV_URL/v1/health returns its encoder name and dim.
Authentication
export lines
above are the client’s own hint, added only when $JEV_API_KEY is unset, so a
JEV_API_KEY=wrong jev heads --url ... prints the first line and nothing else.
When the server is not there
ask and train take --timeout SECONDS
for a cold server that is still loading a model, or a training job too big to
finish inside a minute; $JEV_TIMEOUT sets it for all four remote commands,
including the two with no flag:
jev encoders
Lists the registry and marks the default. No flags, no --url, no --json. Works
in an API-only install.
toy and toy-calibrated are the stand-ins this page’s transcripts run on, registered
by a sitecustomize.py as described in About the transcripts;
a stock install lists the seven above them and nothing else.
(toy and toy-calibrated are there because every transcript on this page runs with the
sitecustomize.py shim described under Custom encoders and the
CLI. A plain install lists the seven built-ins only.)
The * follows $JEV_ENCODER when it is set, and the footnote says so, which is
how you catch an override you forgot was exported:
ask or train that uses it.
Custom encoders and the CLI
jevimage.register(name, factory) is a Python call, and jev never imports your
code, so a registered encoder is invisible to the CLI unless you arrange for your
module to be imported at interpreter startup. The error says as much:
- Put a
sitecustomize.pyonPYTHONPATHthat imports your module and callsregister(). This is what the transcripts on this page do to gettoy. - Do not use the CLI for it. Build the
Jevin Python and serve it:uvicorn-hostingjevimage.server.build_app(jevimage.load(MyEncoder())).
jev ask
Flag syntax is checked before any weights load - a malformed
name=description
pair, a repeated --choice name, or three --noul captions costs you no model
download. The remaining bounds (option and level counts, an unknown head name) are the
core’s, and it sees the question after the encoder is in memory.
Question ids
--choice becomes choice, a second --choice becomes choice2, and so on
per type. A --head question is named after the head. Ids are how you find an
answer in --json output.
confidence, then every class with its
probability and a bar. A score prints 1.28 / 2, the expected level out of the top
index, not a percentage. A head answer also carries its provenance: how many examples
fitted it and its held-out accuracy, so a suspiciously confident answer from a head
trained on nine images says so on the same line. noul prints one number and its basis,
either contrast pair or sigmoid readout.
--json
The table is for humans; --json is the contract. It prints exactly what the
Python API returns, which is what you pipe into jq.
--json prints no header line, so it does not record whether a server
or a local encoder answered.
Failure modes
--noul needs a calibrated encoder. The one-caption form
turns a cosine into a probability through a sigmoid, which requires the encoder
to publish both a logit_scale and a logit_bias. SigLIP has both. CLIP-family
models have a scale but no bias, and an ensemble has no bias either. It averages
its members’ scales, but a bias fitted against one member’s own negatives does not
transfer to a concatenated cosine. Rather than invent a bias and report a wrong
number, it refuses:
$JEV_ENCODER is set in one shell and not another.
jev train
FOLDER has one sub-folder per label.
--encoder is ignored with --url, exactly as on ask: the head is trained in the
server’s embedding space, and stamped with the server’s encoder name. --epochs
defaults to 500; both sides require at least 1, and a server caps it at 5000. Locally, the head lands in
$JEV_HEADS_DIR (see the environment table); with --url it stays on the server
and only its metadata comes back. Raise --timeout for a folder big enough that
uploading and encoding it outlasts the 60s default.
trained is held-out k-fold accuracy (k = min(5, the thinnest class)), never the
fit. zero-shot is what plain prompts scored on the same images. delta is the
answer to “did training actually help”, per class, and it is allowed to be
negative. When the overall delta is zero or below, the command says so instead of
letting you read a table and conclude the opposite:
- in any accuracy column means “not measured”, not zero: k-fold reports no
accuracy for a class too thin to hold anything out.
The same command with --url (or $JEV_URL) trains on the server. Identical
output, identical numbers; the images are uploaded once:
Failure modes
--epochs 0 is refused either way,
because a head fitted in zero steps is all zeros and would still be saved and listed.
The other bounds are the core’s, and apply locally and remotely alike: at least 2
examples per class (k-fold has to hold one out), at most 4000 examples and at
most 100 classes per head.
Head names become filenames and are checked rather than sanitised, because
silently rewriting a name means ask cannot find what train said it saved:
jev heads
--url. An empty result is not an error:
Unreadable head files
A.pt file that will not load is reported on stderr and the listing
continues. It is not silently skipped, because a later “no head named x” about a
file sitting right there in the directory would be a lie:
--json carries the same file under its
broken key, so a script reading only stdout hears about it too.
Three things put a file here: it will not load at all; it loads but is not a head
(someone else’s checkpoint, a bare tensor - anything without W/classes/n);
or its filename is not a valid head name, so no command could address it. All
three are reported the same way and none of them stops the listing.
A directory that cannot be read is a different thing and is an error, not an
empty listing - jev heads and jev rm both say Permission denied rather than
disagreeing about whether the head is there.
Exit status stays 0. A broken file is a warning, not a failure of the listing.
--json
heads is the listing, directory is the same last
line the table prints, and broken appears when a .pt file would not load.
That is the one channel a --json script has for the warning the table puts on stderr.
It is absent when nothing is broken, as above. With junk.pt back in the
directory (heads unchanged, cut short here):
bytes is what the weights would cost: 136 bytes for a 16-dim, 2-class head.
Real encoders are 768 to 1536 dims, so tens of kilobytes.
jev rm
jev serve
pip install 'jevimage[serve]'. Defaults: host
127.0.0.1, port 8000, the default encoder, the default heads directory, no
authentication.
--encoder sets JEV_ENCODER, --heads-dir sets JEV_HEADS_DIR, --api-key
sets JEV_API_KEYS. The same knobs therefore work when a container or a process
manager starts uvicorn itself:
jev serve that has printed “Application startup complete” has its
weights in memory.
--api-key takes several keys, so clients can migrate to a new key before the old
one is withdrawn. Any one of them authenticates; anything else is a 401. Withdrawing the
old key is a restart - the list is read once, at startup, from $JEV_API_KEYS - and the
port refuses connections while the encoder reloads, so the rotation is seamless for
callers but the revocation is not free. See
Serving for the systemd shape. The flag also repeats, so
--api-key old --api-key new is the same as --api-key old new:
--api-key the API is open to anyone who can reach the port, including
POST /v1/heads, which trains, and DELETE /v1/heads/{name}, which deletes.
--host 0.0.0.0 with no key exposes both. GET /v1/health stays open even when
keys are set, so a load balancer does not need a secret to ask whether the
process is alive.
A key is one argument with no space or comma in it. $JEV_API_KEYS - which is how
--api-key reaches the server process - is split on both, so a passphrase like
correct horse battery staple would authenticate as four short keys while the
passphrase itself got a 401. That is refused at parse time, as is an empty key:
--api-key "$JEV_KEY" with $JEV_KEY unset asked for authentication, and starting
an open server instead is the one outcome that must not be silent.
Failure modes
Things the CLI does not do
Know these before you build a script on top.- No
--api-keyflag on the client commands.$JEV_API_KEYonly. - No
--heads-dirflag except onserve.$JEV_HEADS_DIRor$JEV_HOME. - No
--temperature.askuses1 / logit_scale(the reciprocal of the encoder’s learned scale), or 0.01 for an encoder that publishes none. Passtemperature=tojevimage.load()if you need to sharpen or flatten the distributions. - No batch mode. One image per
ask. The HTTP API takes up to 8 per request; a loop overjev askre-encodes the text captions every time. For many images, write the four-line Python loop; see Python API. - No
comparecommand.jev trainprints the comparison once; to re-run it later, usejev.compare(name)in Python orGET /v1/heads/{name}/compare. --jsonis only onaskandheads.trainandrmprint prose.
← Serving · Docs index · Python reference →