Every public name in jevimage, with its exact signature, what it returns, what it raises, and a one-line example. Nothing here is aspirational: every snippet below was run, and its output pasted back, shortened only where a long absolute path or a repeated float would obscure the point, never altered. For the ideas behind the API (why answers are probabilities, why a head is linear), start at index.md. For the same surface over HTTP see reference-http.md, and for the shell see reference-cli.md. About the numbers on this page. Real encoder weights are a multi-gigabyte download, so every example runs against a deterministic 16-dimensional toy encoder (name = "toy", dim = 16, no logit_scale/logit_bias) and the 12-image red/blue ds/ dataset that examples/make_fixtures.py writes. The shapes, types, keys and error messages are exactly what you get from SigLIP2; the probability values are the toy’s and mean nothing about images. Where a number would be misleading if you read it as a model result, it is called out.

The public surface

load, Jev, Encoder, register, available, DEFAULT and JevError are resolved lazily by a module __getattr__, so import jevimage never imports torch. Only load and Jev need it; the rest are lazy purely to keep import time down, and they work on an API-only install. The object connect() returns is jevimage.client.Remote. It is deliberately not exported; you get one from connect(), you do not construct one by name.

On an API-only install

pip install jevimage brings pillow and the standard library, nothing else. Everything above works except load/Jev, which say so:
See install.md for the four install paths.

Entry points

load

Loads an encoder into this process and returns a Jev. Raises ImportError if torch/transformers are missing, KeyError for an unknown registry name, and ValueError for a non-positive or non-finite temperature. Instances are cached per registry name, so calling load("siglip2-base-224") twice loads the weights once.
Note the exception type for a bad name is KeyError, not ValueError:
jevimage.Jev(...) takes the identical arguments; load() is the name to use.

connect

Returns a Remote pointed at a jev serve instance. Downloads nothing, imports no torch. Raises ValueError immediately if url has no scheme. That is the only validation done at construction time, because nothing else can be checked without a round trip:
A keyed client reprs as <Remote 'http://…' keyed>. The key itself is never printed. Remote keeps one connection open and reuses it. A TCP handshake costs a full round trip, which against a server on another continent is around 200ms, an order of magnitude more than the model spends answering, so paying it once per call would dominate every measurement. Calls are serialised on a lock, so one instance is safe to share between threads; for genuine upload concurrency, give each thread its own. A connection the server has since closed is reopened once, silently, for a GET or a DELETE. A POST (ask, embed, train) is not replayed - the failure can surface after the server has already done the work - so it raises JevError and you decide whether repeating it is safe. Release it with close(), or use the client as a context manager:

Jev

The local object: a loaded encoder plus its head directory. Thread-safe for reads; train and delete_head write files. Attributes: .encoder (the encoder object), .heads (a HeadStore, a real dict of name → raw record including the weights), .engine (the readout engine). Methods: ask, ask_embedding, embed, train, compare, head, list_heads, delete_head.

Jev.ask

Encode image once, then answer every question against that one embedding. Returns {question id: Answer} in the order the questions were given. Raises ValueError for a malformed question, an unknown head, a bad temperature, duplicate ids, or a non-finite result; KeyError if a head question names a head file that is present but will not load; TypeError if questions is neither a dict nor a list, or if image is a list rather than one image. Jev and Remote refuse it with the same first sentence; the advice differs, because ask_embedding is local-only:
Question shapes are covered in full in questions.md. The four type values are choice, score, noul and head; every type also accepts an optional "prompt" template that must contain {}.

Jev.ask_embedding

The same, against an embedding you already hold. Local only. There is no remote equivalent, because the entire point is skipping the network hop that Remote is. Use it to ask more questions later about an image you embedded earlier. embedding is a (dim,) or (n, dim) fp32 tensor; only the first row is read. It must be L2-normalised; the engine checks and refuses otherwise.

Jev.embed

One image or a list of them → an (n, dim) fp32 L2-normalised tensor. This is the whole cost of a request; everything downstream is a matmul, so cache what this returns if you will ask about the same image again.
A single image always comes back as (1, dim), not (dim,). There is no cap on the list length locally.

Jev.train

Fit a linear head on the frozen encoder and save it to heads_dir. Returns a Head whose .comparison is already filled in. Raises ValueError for fewer than 2 distinct labels, more than 100 classes, any class with fewer than 2 examples, labels differing only by surrounding whitespace, more than 4000 examples, or an invalid name.
accuracy is held-out, from stratified k-fold with k = min(5, thinnest class), never the fit. It is None only when no fold was possible, which means a class with fewer than 2 examples, something train() refuses outright, so a head trained here always carries a number. (50.0% here is the toy encoder hashing pixel bytes on twelve squares; it is not a claim about SigLIP.) Failure modes worth knowing:

Jev.compare

Did training actually help, per class, against zero-shot prompts, on the same images. Returns None for a head saved without its embeddings. Raises KeyError if no such head, ValueError if the head was fitted under a different encoder.
The two sides are not scored the same way, and basis says so in every response. That asymmetry favours zero-shot, which is the right bias for a “should I have bothered” question. template changes the prompts the baseline is built from, which moves the zero-shot side. On the toy data, "a close-up photo of {}" moves it from 0.75 to 0.5 and turns the verdict from delta: -0.25 into delta: 0.0. A zero-shot baseline is only as good as the prompt you compare against.

Jev.head

One saved head’s metadata. .comparison is always None here. Computing it means re-scoring the stored embeddings, so it is done by train() and compare(), not by this lookup.
A head file that exists but will not load is reported as unreadable rather than as missing. “unknown head” about a file sitting right there is a lie:
list_heads() omits such a file entirely; jev.heads.broken is the {name: why} map if you need to see them.

Jev.list_heads

Every saved head as a plain dict, sorted by name, re-reading the directory first so a head another process trained a moment ago is visible. Weights are never included.
created is a Unix timestamp; bytes is the size of the weight matrices in fp32.

Jev.delete_head

True if a file was removed, False if there was nothing to remove. Raises ValueError for an invalid name. Deleting twice is not an error:

Remote

What connect() returns. Same method names, arguments and return types as Jev, so moving a script from a local encoder to a server is a one-line change. It keeps one connection open. Calls are serialised on a lock, so one instance is safe to share between threads; for genuine upload concurrency, give each thread its own, and release the connection with close() or the context manager. Attributes: .url (normalised, no trailing slash), .api_key, .timeout. Methods: health, ask, embed, train, compare, head, list_heads, delete_head.

What differs from Jev

Everything else matches, including Answer objects out of ask, a Head out of train, None out of compare for a head with no stored embeddings, and False out of delete_head for a head that was not there.

Remote.health

What the server is running. Never requires the API key, so a load balancer without the key does not report a live process as dead.
This is how you find out remotely which encoder your heads will be trained under. It does not tell you whether single-caption noul will work; that needs the server’s logit_scale/logit_bias, which /v1/health does not report. Ask a single-caption noul and read the error, or use the {"true": …, "false": …} pair form, which works on every encoder.

The rest, verified against a running server

train() uploads every image once; the head and its weights stay on the server, and what comes back is metadata. A head name is validated on this side before a URL is built out of it:

Answer

A dict subclass, so it serialises to JSON unchanged, with attribute access because a.choice reads better than a["choice"]. repr() is a one-line summary; the full shape is json.dumps(answer).
A missing key raises AttributeError naming what the answer does have:

Shapes per question type

Probabilities are rounded to 6 decimals, score to 4. Real examples of each:
basis tells you which noul form ran. The {"true": …, "false": …} pair gives "contrast pair" and works on every encoder. A single caption gives "sigmoid readout" and needs an encoder publishing both logit_scale and logit_bias. SigLIP does; CLIP-family models and every Ensemble do not, and say so rather than returning a number that looks like a probability and is not one:

The metadata of a trained head. The same object comes back from a local train() and a remote one, because a head is metadata plus numbers; the weights stay wherever the encoder is, and never cross the wire. to_dict() carries four fields with no property of their own: folds, created, dim, bytes.
Note the key is cv_accuracy in the dict and .accuracy on the object. to_dict() is also exactly what list_heads() returns per head and what the HTTP API serves. A head records its encoder and is checked, not trusted:
The same check guards compare(). It compares the encoder name, not just the width, because two encoders of equal dimension would otherwise produce confident nonsense.

Encoders

Encoder

The interface. Subclass it, or duck-type the four required members and pass the object straight to load(); registration is optional. The normalisation requirement is enforced, not assumed. A rescaled embedding would saturate the softmax to exactly 1.0 and 0.0:
jevimage.encoders.unit() does that for you: it pulls the tensor out of whatever a model returned, mean-pools token-level output, and normalises in fp32.

register

Add an encoder to the registry. factory is called with no arguments the first time load(name) asks for it, so the weights load lazily. Re-registering a name replaces any cached instance. Raises ValueError if name contains '+' (reserved for ensembles) and TypeError if factory is not callable.
Registration lives in your process. The jev CLI never imports your code, so a registered encoder is invisible to it; see encoders.md.

available

Registry names, sorted. Ensembles are formed on demand from 'a+b' and are not listed. Needs no torch, so it works on an API-only install.

DEFAULT

The encoder load() uses when given neither an argument nor $JEV_ENCODER.

Image and data helpers

All four are pure pillow/stdlib and available on every install.

to_image

Accepts the four things people have, and refuses anything ambiguous:
A plain string that is neither a path nor a data URL is an error rather than a guess at base64, so a typo’d filename says so instead of failing three layers down. This function does not restrict the format. IMAGE_SUFFIXES governs folder scanning, and the HTTP server enforces its own format allowlist.

read_folder

One sub-folder per label, the layout every image dataset already uses. Recurses into each label folder, keeps only IMAGE_SUFFIXES, sorts both labels and files. Loose files directly in folder are ignored, because they have no label.

as_examples

Normalises the three shapes train() accepts into parallel (images, labels) lists. Labels are always stringified.
A str/Path is passed to read_folder first. max_per_class applies to all three shapes: for a list of (image, label) pairs it counts per label and keeps the first N of each. Images are not opened here; that happens at embed time.

confidence

How peaked a distribution is, rescaled so uniform is 0.0 and certain is 1.0: (max(p) - 1/k) / (1 - 1/k). p is any sequence of probabilities; k = 1 returns 1.0.
Raw max-probability is not comparable across questions. 0.5 is lukewarm over two options and emphatic over fifty. This rescaling removes that dependence, so one threshold works for every question you ask. choice, score and head answers carry it already; this function is for distributions you built yourself.

IMAGE_SUFFIXES

The extensions read_folder picks up (sorted here because it is a set and its repr order changes between runs). Deliberately short: an animated GIF, an SVG or a RAW file is not a thing to silently take the first frame of. It is a plain set, so you can add to it, at your own risk, since the encoders and the HTTP server have their own format expectations.

__version__

Also reported by Remote.health()["version"], which is how you check that a server and a client agree.

JevError

Raised by every Remote method for a request the server refused or could not reach. It subclasses ValueError on purpose: a local Jev raises ValueError for these same refusals, so an existing except ValueError keeps working when load() becomes connect().
str(e) adds the status, method and URL for a log line; .detail is the part a human needs. Typical statuses: The None case is the connection failure, and it names the port and what to run:
A timeout raises JevError too, suggesting timeout=. Two statuses are deliberately swallowed so Remote matches Jev: a 404 from delete_head() becomes False, and a 409 from compare() becomes None.

Limits and errors

These ceilings are enforced by the engine. They are module constants, not part of the public API, but hitting one produces a message you may need to recognise. Every message below is verbatim. All of them are ValueError unless marked otherwise. Answer attribute misses raise AttributeError; to_image raises FileNotFoundError, IsADirectoryError, ValueError (a malformed data: URL) or TypeError; read_folder raises NotADirectoryError.

Temperature

The divisor applied to the logits before the softmax. It affects choice, score, the noul pair form and head; it does not affect the noul sigmoid readout, which is calibrated by the encoder’s own scale and bias. Resolution differs between the two cases, because they are not the same kind of logit:
A head question reacts to the same argument, from a base of 1.0:
Higher flattens, lower sharpens. The default for an uncalibrated encoder is 0.01, which is the CLIP-family convention (a raw cosine lives in roughly [0, 0.3] and softmaxes to nearly uniform without it). A calibrated encoder’s own logit_scale is the temperature it was trained to be read at, so overriding it costs you the calibration that makes the number mean something. Change it only if you have measured the result.
← CLI reference · Docs index · HTTP reference →