This release is a pre-release and may not be stable for production use.
jinfer
Local AI inference for Python. Chat, vision, audio, embeddings, reranking, speech and transcription in one wheel: no server, no ML stack, no GPU needed.
jinfer is jinfer, the JVM inference engine, for Python: libjinfer, the engine, every ported model architecture and its native kernels compiled into one shared library with a C API, which the wheel carries.
A GGUF model runs in your process, on the CPU, from one load call, and comes back as a typed object with only the methods the file supports.
toknroll is the only dependency, for the tokenizer.
pip install jinfer
import jinfer
model = jinfer.load("LiquidAI/LFM2.5-350M-GGUF:Q8_0") # a LanguageModel; the file decides
reply = model.chat("Why is the sky blue?", max_tokens=200) # already generating
for text in reply: # stream it ...
print(text, end="", flush=True)
reply.text, reply.usage.prompt_ms # ... or just read it
@dataclass
class Person:
name: str
born: int
person = model.chat("Ada Lovelace, born 1815.", schema=Person).parsed # a Person, guaranteed
def weather(city: str) -> dict:
"""Current weather for a city."""
return {"celsius": 18}
conversation = model.conversation(system="Be brief.", tools=[weather])
print(conversation.chat("Weather in Zurich?").text) # runs weather, then answers
branch = conversation.fork(); conversation.rewind() # free: nothing is recomputed
Why jinfer
- Three lines to a reply, and the reply is an object.
load,chat,.text; no messages list to assemble for a first question, no["choices"][0]["message"]["content"]to dig through, no server to start first. - Everything local in one package. Chat, vision, audio in, embeddings, reranking, speech synthesis and transcription, each a method on the class the model file calls for, from one 19 MB wheel; no
torch, notransformers, no daemon. - Structured output that cannot come back malformed.
schema=Persontakes a dataclass, a TypedDict, a Pydantic class,list[T],LiteralorEnum, the engine enforces it while decoding, andreply.parsedis the Python value; there is nothing to validate and retry. - Tools are functions. The signature is the schema and the docstring the description; a conversation runs the calls itself, between the words of the streamed answer.
- The engine keeps the conversation. A turn costs only its new tokens,
forkandrewindrecompute nothing,preparewarms a prompt before the user types, and a prompt cache file keeps it across processes. - Stopping is not losing. Close a reply early and its partial text, usage and finish reason stay readable, in a conversation too.
- Typed errors, keyword options, model defaults.
InvalidArgumentis aValueError,NotFoundaFileNotFoundError; a typo in an option is aTypeErrorat the call; an option left out takes the model file's value, never the library's. - One engine, three faces. The same code serves Java, this package, and
jinfer.h, twenty-nine C functions any language calls.
| jinfer | ollama | transformers | llama-cpp-python | |
|---|---|---|---|---|
| Install | pip install, no ML stack |
the Ollama app, a running server, pip install ollama |
transformers plus torch, gigabytes |
compiles llama.cpp, or a per-backend wheel index |
| Lines to a first reply | 3 | 3, plus two shell commands | 4 | 4 |
| Where the text is | .text |
.message.content |
[0]["generated_text"][-1]["content"] |
["choices"][0]["message"]["content"] |
| Stop early | close; partial text, usage and finish reason stay | break; nothing stays | custom stopping criteria | break; nothing stays |
| Conversation state | an object; engine cache visible; fork, rewind | your list, re-sent each turn | your list, recomputed each turn | your list, last prefix reused |
| Structured output | any Python type, parsed object back, enforced | JSON schema in, validate yourself | none built in | JSON schema in, dict back, enforced |
| Tools | functions; the conversation runs the loop | functions; you run the loop | functions; you parse and loop | hand-written JSON; you parse and loop |
| Embeddings, reranking | (n, dim) float32 array; one call |
list of lists; none | pooling by hand; cross-encoder by hand | list of lists; none |
| Speech, transcription | both, streaming, timed words | neither | both, no streaming | neither |
| Extra process | none | required | none | none |
| GPU | no | yes | yes | yes, chosen at install |
| Model breadth | the architectures jinfer ports | the Ollama library | everything on the Hub | every GGUF llama.cpp loads |
Where it trails, so you can decide: the CPU only, the architectures listed below rather than every GGUF, WAV or PCM into transcribe, and eight options on purpose, the rest left to the model file.
The comparison, use case by use case, has every snippet.
Requirements
- Python 3.10 or later, and the
toknrollwheel it pins, installed with it. - The wheel carries the library: Linux x86-64 and aarch64 with glibc 2.28 or later, macOS 14 or later on Apple silicon, Windows 10 or later on x86-64. On x86-64 the library is built for x86-64-v3, a CPU with AVX2.
curlon the PATH, to download a model named by a hub reference; a local file needs nothing.ffmpegon the PATH, to decode an image or an audio file given to a chat; a WAV file for transcription needs nothing.espeak-ng(orespeak) on the PATH, for speech synthesis with Kokoro voices.- RAM for the model file, which is read whole, plus its context.
Models
Any GGUF of a ported architecture, in Q4_0, Q4_1, Q5_0, Q5_1, Q4_K, Q5_K, Q6_K, Q8_0, MXFP4, NVFP4, Q1_0, TQ1_0, TQ2_0, F32, F16 or BF16; Q8_0 is the recommended balance of quality and speed.
| Family | What the model does |
|---|---|
| Google Gemma 4 | chat, vision, audio in |
| Google EmbeddingGemma 2 | embeddings of text, images, audio and video |
| Liquid AI LFM 2.5 | chat, vision, embeddings, reranking |
| Liquid AI LFM3 | chat, vision, audio in and speech out (LFM3-Omni) |
| OpenAI gpt-oss | chat |
| Alibaba Qwen 3.5+ | chat, vision |
| Alibaba Qwen 3 | embeddings, reranking |
| Meta Llama 3+, IBM Granite 4.1+, Mistral AI Ministral 3, Hugging Face SmolLM3, OpenBMB MiniCPM5 | chat |
| Poolside Laguna XS 2.1, JetBrains Mellum 2, inclusionAI Ling 3, NVIDIA Nemotron-H | chat |
| Kokoro, Inflect | speech synthesis |
| NVIDIA Parakeet | speech recognition, with timed words |
The jinfer README keeps the authoritative table, with links to each family.
Models are hub references (owner/repo:QUANT, downloaded once and cached) or local paths.
load returns the class the file calls for, LanguageModel, Embedder, Reranker, Speaker or Transcriber, each with only the methods that apply.
Every call returns a plain typed object; nothing is a dict you have to know the shape of, and nothing in the API mentions tokens, JSON or OpenAI field names.
| You want | Call |
|---|---|
| a reply | model.chat(prompt).text; print(reply) prints the text |
| a streamed reply | for text in model.chat(prompt), or async for; close the reply to cancel, what it produced stays readable |
| the rest of a reply | reply.tool_calls, .reasoning, .finish_reason, .usage (tokens, cached tokens, milliseconds) |
| a system prompt | model.chat(prompt, system="You are terse.") |
| options | max_tokens, temperature, top_p, seed, stop, tools, tool_choice, reasoning_effort; keyword-only, None is the model's default, a typo or a wrong type is a TypeError |
| structured output | model.chat(prompt, schema=Person).parsed: a dataclass, TypedDict, Pydantic class, list[T], Literal, Enum, dict (free-form) or a JSON schema dict; it always parses, there is nothing to retry |
| tools | tools=[fn]: the signature is the schema, the docstring the description; a conversation runs the calls, a plain chat returns them as reply.tool_calls; tool_choice is "auto", "none", "required", a function or its name |
| a conversation | conversation = model.conversation(system=...); .chat(text, schema=...) streams like model.chat, .fork(), .rewind(), .prepare(), .messages |
| images, audio in | a Path or bytes in the content list: model.chat(["What is this?", Path("photo.jpg")]) |
| embeddings | embedder.embed(texts) gives (n, dimension), a single string counts as one; a text is embedded whole, up to context_capacity: 4096 tokens by default, or the model's maximum when smaller |
| reranking | reranker.rerank(query, documents) gives one score per document |
| speech | speech = speaker.speak(text, speed=1.2); iterate PCM chunks, or speech.audio, speech.save("out.wav") |
| transcription | transcriber.transcribe("speech.wav") or (pcm, sample_rate=16000); .text and timed .words |
| tokens | model.tokenizer is a toknroll.Tokenizer: encode(text) gives an array('i'), decode(ids), count_tokens, encode_batch, vocabulary_size, token(id), id(token); toknroll.load(source) gives one without the weights |
| token budgets | len(model.tokenizer.encode(text)) against model.context_capacity (the state in use) and model.max_context_length (the model's bound) |
| the context size | jinfer.load(source, context_capacity=8192): the tokens of context the model's states hold, up to its maximum; the default (0) is 4096, or the maximum when smaller, for every kind of model, jinfer.MAXIMUM_CONTEXT (-1) is the maximum, above it is refused |
| log lines | logging.getLogger("jinfer"), as many as JINFER_LOG lets through: off, error, warn (the default), info (downloads), debug (loads, kernels) |
Errors are jinfer.JinferError subclasses: InvalidArgument (also a ValueError), NotFound (also a FileNotFoundError), Cancelled; each has .code.
jinfer.OpenAI(ref) is a migration aid: code written for the OpenAI SDK's chat.completions.create and embeddings.create runs in-process, unchanged.
Behaviour worth knowing
- A reply is already generating when
chatreturns. Iterating and the properties are two views of the same reply: read.textafter a partial iteration and you get the whole reply. - Closing a reply early cancels it, and the reply keeps what was produced:
.textis the partial text,.finish_reasonis"cancelled",.usagecounts what ran. In a conversation the partial text joins the history; a turn cancelled before its first word leaves no trace. - A conversation turn with functions among the tools streams too: the calls run as you read the reply, between the words, and the text you iterate is the model's answer after them.
reply.usageis the whole turn's. - Temperature 0 gives the same tokens every run. Two different kernels can still land on different sides of a near-tie between two tokens, so a test pins behaviour, not prose.
- A conversation's turns are served from the engine's kept state, so a turn costs only its new tokens;
reply.usage.cached_tokensshows it. A fork shares the prefix; a rewind recomputes nothing and keeps the system prompt;prepare()computes the prefix before the user types. Withprompt_cache=path, what a model prepared is written when it closes and read by the next one. - Structured output is enforced while decoding, so malformed JSON cannot come back. Validate meaning if you need to; there is no format to retry.
- Quiet by default: only warnings and errors reach
logging. A download shows its progress on stderr, jinfer's live board on a terminal and a line at each tenth elsewhere;JINFER_PROGRESS=off, or-Djinfer.progress=offthroughconfigure, turns it off entirely. With progress off,JINFER_LOG=infologs each download instead, a start and a done record, shown once the application logs at INFO (logging.basicConfig(level=logging.INFO));JINFER_LOG=offsilences the library. Ctrl-C during a download ends the process; the download resumes next time.repr(model)names the kernels and threads that actually run. - Any thread may call. A model may be shared between threads; a reply, speech or conversation belongs to one thread at a time.
async foron a reply or speech steps in a worker thread; wrap one-shot calls inasyncio.to_thread. - A model's weights stay alive while any reply or speech made from it lives;
close()drops your reference. Context managers close; so does garbage collection. - Embeddings and audio are float32
array.arraybuffers, scores are floats: the standard library, whoever installs the package.np.asarray(vector)andtorch.frombuffer(speech.audio, dtype=torch.float32)are zero-copy. jinfer.configure("-Djinfer.threads=8", "-Djinfer.cache.sessions=16", "-Djinfer.cache.budget=8g", "-Djinfer.offline=true", "-XX:MaxHeapSize=4g")passes the runtime the options a java command line would, once and before the first load; the engine'sjinfer.*andjam.*properties are all reachable this way. A later call is refused, and a rejected option fails this one.JINFER_OPTIONS="-Djinfer.threads=4"in the environment holds the same options and overrides them, so the box a program runs on can retune it without a code change.- A scalar schema (
int,str,bool,Literal,Enum) travels as{"value": ...}on the wire, so the model's first token is anchored;reply.parsedis the value,reply.textthe JSON. Objects and lists go as they are.
From C
The wheel is the C library too: jinfer.h, toknroll.h, libjinfer and a pkg-config file live in the installed package, and the package says where.
On Linux and macOS, compile with Python's standard library, preserving each output line as one compiler argument:
import subprocess
flags = subprocess.check_output(
["jinfer-config", "--cflags", "--libs"], text=True,
).splitlines()
subprocess.run(["cc", "chat.c", *flags], check=True)
jinfer-config is the wheel's console script, in the venv's bin, with --cflags, --libs, --pkgconfigdir and --version, as numpy-config and python3-config have them.
Compiler arguments are unquoted, one per line; keep spaces within each line rather than splitting on whitespace.
For pkg-config, point it at the installed package:
PKG_CONFIG_PATH="$(jinfer-config --pkgconfigdir)" pkg-config --cflags --libs jinfer
pkg-config emits shell-escaped flags; in a Python build script, parse that output with shlex.split() rather than splitlines().
On Windows the flags are cl's, /I and /LIBPATH with jinfer.lib, one per line, and jinfer.dll sits beside the import library.
The C API is jinfer.h; nothing else is a tool here, jbang jinfer@qxoticai is.
With toknroll
libjinfer carries its own toknroll: a model's tokenizer lives in jinfer's runtime, and model.tokenizer is a toknroll.Tokenizer served by libjinfer.
The toknroll wheel is a dependency for that class alone, pinned to the exact toknroll inside libjinfer; the class checks the library's version when it binds, so a mismatched environment fails at import jinfer naming both versions rather than later.
jinfer.configure and JINFER_OPTIONS configure the model's tokenizers too, -Dtoknroll.* options included, as they configure everything in libjinfer.
toknroll.configure and TOKNROLL_OPTIONS configure libtoknroll only, which jinfer never loads.
Using both wheels in one process is fine: toknroll.load(...) starts libtoknroll, a second runtime with its own heap and threads, the two never interact, the order they start in does not matter, and every tokenizer remembers the library serving it.
A knob both need, such as a GGUF pre-tokenizer registration, is given to both.
Examples
One script a use case, in examples/, each runnable as it is with the model it names, downloaded on the first run:
| Script | Model |
|---|---|
chat.py, stream.py, conversation.py, structured.py, tools.py, openai_client.py |
LiquidAI/LFM2.5-350M-GGUF:Q8_0 |
vision.py |
LiquidAI/LFM2.5-VL-450M-GGUF:Q8_0 with companions={"media": ...}, the repository's mmproj file |
embed.py |
LiquidAI/LFM2.5-Embedding-350M-GGUF:Q8_0 |
rerank.py |
mradermacher/Qwen3-Reranker-0.6B-GGUF:Q8_0 |
speak.py |
simonfxr/kokoro.cpp-GGUF:Q8_0 with companions={"voice": ...}, one of the repository's voice files |
transcribe.py |
mudler/parakeet-cpp-gguf/tdt-0.6b-v3-q8_0.gguf |
A companion is a second file a model needs, named at load: jinfer.load(ref, companions={"media": "<ref>"}) for a vision model's projector, {"voice": "<ref>"} for a speaker's voice.
The architectures the library carries are jinfer's ports, listed in the jinfer documentation.
From a checkout
make python in jinfer/jinfer-native stages the freshly built library into the package, then pip install python/.
JINFER_LIBRARY=/path/to/libjinfer.so points the package at any build.
pydantic is optional.
The C API the package binds is jinfer.h, with toknroll.h for the tokenizer, usable from any language with a foreign function interface.
Links
- jinfer: the engine, the ports, the Java API, the server and the framework providers.
- libjinfer: the shared library, its C API
jinfer.h, this package and its examples. - Documentation, the Python comparison, and issues.
License
Apache 2.0, by Quixotic AI, for the package and the library's own code. The library in the wheel is built with Oracle GraalVM and carries its Native Image runtime under the GraalVM Free Terms and Conditions, spelled out in the wheel's NOTICE: use it as you like, commercially too; a product sold for a fee that bundles a copy of it is not covered by those terms, and takes a build with GraalVM CE, see the libjinfer README.
Metadata
Release files for jinfer 0.4.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| jinfer-0.4.0a1-py3-none-manylinux_2_34_x86_64.whl | Python 3 | none | Linux glibc 2.34+ x86-64 | Details for jinfer-0.4.0a1-py3-none-manylinux_2_34_x86_64.whl |
Release files / jinfer-0.4.0a1-py3-none-manylinux_2_34_x86_64.whl
| Download URL | jinfer-0.4.0a1-py3-none-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 18.1 MB |
| Tags | Linux glibc 2.34+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
08d9179b642010ba38bd6a0cfe7727b3e4f85e6280152f4b5c3bd1618fea0716
|
|
BLAKE2b-256 checksum How to use checksums |
d2194d533f0febc788fcd693ae6c644153eea792942c81d8abf4cd8364b08b46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for jinfer 0.4.0a1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.