Skip to main content

geistr

Run language models on your own computer: download, chat, serve. One small binary, no Python, no Docker, no account. The model runs locally, on the CPU or the GPU.

Full manual: docs/GUIDE.md: every command, chat command, setting and file, the HTTP API, and what to do when something fails.

geistr chat in action: qwen3-0.6b answers on the CPU, /model switches to gemma4-e2b on the GPU for a table, and a new session continues the conversation

Install

curl -fsSL https://www.geisten.net/download/geistr/install.sh | sh

Or with Homebrew:

brew install geisten/tap/geistr

Both work on Linux (x86_64, arm64) and macOS (Apple Silicon). The script downloads the latest release, checks it against SHA256SUMS and puts geistr in /usr/local/bin.

Without sudo, or a specific release (the server keeps the last 5, from 0.3.2 on; another one is reported as "no release …"):

curl -fsSL https://www.geisten.net/download/geistr/install.sh | PREFIX=~/.local sh
curl -fsSL https://www.geisten.net/download/geistr/install.sh | GEISTR_VERSION=v0.4.0 sh
Requirements

The Linux binaries are fully static and run on any distribution. The CPU needs x86-64-v3 (AVX2: Intel since 2013, AMD since 2015) or ARMv8.2 with dotprod (Raspberry Pi 5, AWS Graviton 2 and newer); on an older CPU geistr says so and exits. To build from source instead, see docs/DEVELOPMENT.md.

Quick start

geistr chat                    # the first time: the models that run here, the best
                               # first; choose with ↑↓ and Enter, it downloads, verifies
                               # and chats

Or choose yourself:

geistr catalog                 # which models fit this computer
geistr chat qwen3-0.6b         # talk to it; not installed yet: Enter downloads it (640 MB)

geistr pull qwen3-0.6b downloads without starting anything: for scripts and servers (without a terminal geistr never downloads unasked), the embedding model of --files, and with no id, the update of all installed models after a new geistr.

Or a single answer, for scripts:

geistr run qwen3-0.6b "Name three prime numbers."
echo "Summarize: …" | geistr run qwen3-0.6b

Models

geistr catalog lists the models geistr knows, the best suited to this computer first (installed or not does not move a row), marks what is installed (✓), what is available (↓) and whether each fits your RAM (⚠/✗), with the speed measured here on ⚙ CPU and ⚡ GPU.

model download RAM
smollm2-360m 0.4 GB 2 GB
qwen3-0.6b 0.6 GB 4 GB
qwen35-0.8b 0.8 GB 4 GB
bitnet-2b 1.2 GB 4 GB
gemma4-e2b 3.1 GB 8 GB
gemma4-e4b 5.0 GB 16 GB
bonsai2-27b-pq2 7.2 GB 24 GB
qwen38-27b-q4 16 GB 32 GB
qwen38-27b-q8 29 GB 48 GB

Every download is checked against its SHA-256 and resumes where it stopped. Any other .gguf file works too: geistr chat path/to/model.gguf.

Chat

In a terminal, geistr chat renders Markdown, tables, math and clickable links, shows the speed after each answer and continues your last conversation (see below). Once the context is half full, the prompt says how full (⚙ 62% >, yellow from 80 %, red from 95 %); /info always shows it. Type / for the commands:

/model gemma4-e2b    switch the model, keep the conversation (best fit first)
/gpu /cpu /auto      switch the processor (Metal on macOS, Vulkan on Linux)
/temp 0.7            sampling temperature
/system Be brief.    system prompt (/system off removes it)
/info                what runs now, and how full the context is
/files ~/Notes       answers from your files (/files off: none)
/retry               the last answer again (at temperature 0: once at 0.7)
/copy  /copy code    the last answer, or its last code block, to the clipboard
/save                keep model, processor, temperature and system prompt
/clear  /exit
key does
Ctrl-J, Alt-Enter, \ + Enter a new line in the message (Enter sends)
Esc stop the answer
Ctrl-C clear the line; twice on an empty line: exit
↑ ↓ earlier lines, also from earlier chats; → takes the hint
Ctrl-R search earlier lines (type to narrow, Ctrl-R for older, Enter takes it)
? the shortcuts

Your conversation stays

Close the terminal, reboot, come back tomorrow: geistr chat picks up where you left off, and the model still knows what you talked about.

↻ 6 · „Compare Mars and Venus in a small table…“ · /clear new
⚡ >

That is what makes a local model useful for real work: you can think a problem through over several sessions, refer back to an earlier answer, or switch to a bigger model mid-conversation without explaining everything again. It is also private by design: each conversation is a plain file on your computer (readable only by you, one JSON message per line), never sent anywhere.

A long conversation stays fast to resume: the model re-reads only the newest part (about 2000 tokens, geistr config resume_tokens), and the ↻ line says how much. The whole conversation stays in the file. When a conversation outgrows the model's context, the oldest messages are left out, and the chat says so (↥ 12 oldest messages left out …).

  • /clear starts a new conversation and keeps the old file.
  • geistr chat --new starts fresh once; geistr config resume off always.
  • Piped chats (echo … | geistr chat) keep nothing, so scripts stay reproducible.

Ask your own files

geistr pull bitnet-embed-0.6b      # the embedding model, 428 MB, once
geistr index ~/Notes               # text, Markdown, PDF (with pdftotext)
geistr chat --files ~/Notes        # or /files ~/Notes in a chat

Each question gets the most relevant passages of your files put in front of it, and the answer shows where they came from (sources: garden/shed.md). With the reranker installed (geistr pull qwen3-reranker-0.6b, 639 MB) only the passages that answer the question go in front of it and only their files are named; for a question your files do not answer it says nothing relevant in your files. Without it, the four nearest passages are used and named. geistr run --files ~/Notes "…" does the same once. The index is updated by file hash whenever you use the folder, so only new and changed files are read again. It stays on your computer, readable only by you, in the data folder (index/); hidden files and folders are left out.

Decisions

Some questions have a fixed set of answers: which team gets a ticket, is a text urgent. geistr decide reads the model's choice among your options from one pass over the prompt, without writing an answer:

$ geistr decide gemma4-e2b "I was charged twice. Which team handles this?" billing technical sales
billing
  billing    1.00
  technical  0.00
  sales      0.00

Piped, it prints only the chosen option, for scripts. The probabilities are the model's own, read from the option keys; nothing else can come out.

With --threshold 0.9, a decision below that probability is not taken: geistr decide exits with 3, or with --escalate chat the same model writes the answer (thinking, if it can), or with --escalate bonsai2-27b-pq2 a larger model decides. --max-tokens N caps the chat answer and --temperature T applies your calibration. Where to set the threshold and the cap is your application's trade between speed and quality, and it depends on the task and the model. docs/DECISIONS.md shows how to measure it, with one measured curve.

It works with every chat model: the prompt follows the model's own chat template. For gemma4-e2b and bonsai2-27b-pq2 the decisions are also checked against an independent engine (llama.cpp); for other models geistr says "unverified". EXPERIMENTAL: details, limits and evidence in docs/DECISIONS.md.

Use it from other tools

Keep a model loaded and talk to it over the OpenAI or Ollama API, e.g. from Open WebUI, editor plugins or the OpenAI SDK:

geistr serve gemma4-e2b --http          # http://127.0.0.1:11434
geistr serve --http                     # every installed model, one loaded at a time
curl http://127.0.0.1:11434/v1/chat/completions \
  -d '{"messages":[{"role":"user","content":"Hi"}]}'
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:11434/v1", api_key="unused")
reply = client.chat.completions.create(model="gemma4-e2b",
                                       messages=[{"role": "user", "content": "Hi"}])
print(reply.choices[0].message.content)

In Open WebUI, add http://127.0.0.1:11434 as an Ollama connection (or …/v1 as OpenAI). Clients that resend the whole conversation pay only for what is new.

Without a model, geistr serve --http offers every installed model (Open WebUI lists them) and the request's model chooses. It is built for small computers: only one model is in memory. A request for another model waits while the loaded one is unloaded completely and the requested one is loaded (qwen3-0.6b ⇄ gemma4-e2b: 0.5–0.7 s on an M1 Max's GPU; not yet measured on a Raspberry Pi 5); its conversations are then processed anew, which takes longer, but the answers are the same.

Endpoints, the socket, and security
endpoint
POST /v1/chat/completions OpenAI: messages, temperature, max_tokens / max_completion_tokens, stop, stream (SSE, stream_options.include_usage), tools / tool_choice
POST /v1/embeddings OpenAI: input (a text or a list), encoding_format (float, base64); with an embedding model
GET /v1/models the model, or without one every installed model
POST /api/chat Ollama: messages, stream (NDJSON, default), options.temperature, num_predict, stop, tools
POST /api/embed Ollama: input (a text or a list); with an embedding model
GET /api/tags, /api/version, / the model(s), the version, a health check

There is no authentication. By default geistr listens on loopback only and answers only requests addressed to this computer (Host check, against DNS rebinding). A web page in a browser may use it only if it comes from this computer too (Origin check: another site's page cannot start answers); GEISTR_ORIGINS=https://ui.example,… (or *) allows more, like Ollama's OLLAMA_ORIGINS. --http=0.0.0.0:11434 makes it reachable for everyone who can reach the computer; geistr warns about that. One request is answered at a time; a client that disconnects stops its answer.

Tool calling works with Qwen3 models (e.g. qwen3-0.6b): send tools, get tool_calls back (finish_reason: tool_calls; streamed too), send the results as role: tool messages, and the model answers with them, as in the OpenAI SDK's function-calling example. tool_choice: "none" turns tools off for a request. A model without a tool format refuses tools (400).

Images work with Gemma 4 E2B (gemma4-e2b): geistr pull gemma4-e2b also fetches its vision tower (the vision tensors of the original model, a 337 MB vision_tower.safetensors next to it, SHA-256 verified). Send an image_url part with a data: URL (OpenAI) or images (Ollama), PNG, JPEG or BMP, one per message; the image goes with the last message, and the conversation keeps it for the follow-up questions. Other models refuse images (400).

Embeddings: geistr serve bitnet-embed-0.6b --http answers /v1/embeddings (the OpenAI SDK's embeddings.create works) and Ollama's /api/embed: unit-length vectors of 1024 dimensions, multilingual. For retrieval, embed the question and the documents as they are, with nothing before them, as --files does: on scripts/retrieval-bench.py (#157) that put the right file among the four nearest passages most often (97 %), ahead of an Instruct: …\nQuery: task before the question (92 %) or query: / passage: (89–94 %). An embedding model does not chat, and a chat model does not embed (400). geistr serve <model> runs that one model, the one in /v1/models; a request that names another gets 404 (model_not_found) instead of this model's answer. geistr serve --http without a model loads the one a request names; one that is not installed gets 404 (a service downloads nothing: geistr pull it).

Without --http, geistr serve listens on a Unix socket only (owner only, 0600), and geistr chat --socket chats through it. --chats N sets how many conversations the service keeps (default 2), --max-request=MiB the size of one request (default 16). The options and the socket protocol: docs/GUIDE.md.

Settings

Settings are remembered between runs; geistr config shows them and the file they live in.

geistr config processor gpu                # auto (default), cpu, gpu
geistr config temperature 0.7              # 0 to 2
geistr config system "Answer in German."   # system prompt for every new chat
geistr config markdown off                 # plain text
geistr config stats off                    # no speed line
geistr config resume off                   # always start new, keep nothing
geistr config history off                  # don't keep typed lines between chats
geistr config threads 8                    # CPU threads (default: one per physical core)

--cpu / --gpu choose the processor for one run, --threads N the CPU threads, --models DIR another model folder.

All commands
command does
geistr catalog [--installed | --available] [--json] the models, their state, fit and speed
geistr pull <id> download, resume, verify
geistr pull update the installed models after a geistr update
geistr run <model> [prompt…] one answer to stdout; the prompt from stdin if none
geistr chat [<model>] [--new] interactive; without a model the last one
geistr serve <model> [--http[=ADDR:PORT]] [--socket=PATH] [--chats N] [--max-request=MiB] the model as a service
geistr serve --http[=ADDR:PORT] […] every installed model as a service, one loaded at a time
geistr chat --socket[=PATH] chat with that service
geistr config [key [value]] settings
geistr bench [model…] tokens/s on CPU and GPU (default: every installed model)
geistr bench --compare [A [B]] two engine versions' speeds side by side
geistr --version geistr, catalog revision, engine commit

<model> is a catalog id or a path to a .gguf file. Options: --cpu / --gpu, --models DIR, --catalog FILE. Exit codes: 0 ok, 1 error, 2 usage, 130 cancelled.

Every complete answer records its speed in speed.tsv in the data folder; geistr catalog shows the median of the last ten geistr bench runs (of the last ten answers before the first).

Update

brew upgrade geistr, or run the install command again. The model list ships inside geistr, so new models come with a new geistr. Afterwards geistr catalog marks installed models whose file changed with ⟳ and a file of the right size with another checksum (damaged?) with ✗, and geistr pull fetches them (the old file stays until the new one is verified).

Python and C

geistr is built on geist-runtime, an embeddable model runner on geistlib with one C API (include/geistr.h) and a Python package (pip install geistr; every release also has its wheels at https://www.geisten.net/download/geistr/vX.Y.Z/):

import geistr

with geistr.chat("gemma4-e2b", system="Answer briefly.") as chat:
    for piece in chat.send("What is the capital of France?"):   # streamed
        print(piece, end="", flush=True)
    print(chat.ask("And of Italy?"))           # only the new message is processed

Using geistr: docs/GUIDE.md. Building, the library, tests and CI: docs/DEVELOPMENT.md. API reference and design: docs/API.md.

Metadata

Release files for geistr 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for geistr 0.4.0
File Interpreter ABI Platform Actions
geistr-0.4.0-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64, Linux glibc 2.27+ x86-64 Details for geistr-0.4.0-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
geistr-0.4.0-py3-none-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.27+ ARM64, Linux glibc 2.28+ ARM64 Details for geistr-0.4.0-py3-none-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
geistr-0.4.0-py3-none-macosx_15_0_arm64.whl Python 3 none macOS 15.0+ ARM64 Details for geistr-0.4.0-py3-none-macosx_15_0_arm64.whl

Total release size: 2.9 MB

Release files / geistr-0.4.0-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

Download URL geistr-0.4.0-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Size 1.2 MB
Tags Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
3800541f541a4cd4ee75abf3ccb9bad05ae74164a1de80c9b58439364a15b1ed
BLAKE2b-256 checksum
How to use checksums
a380592aba2e41de3f23058c934eee8eca3a226827aef767ffc9a58900b7270c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / geistr-0.4.0-py3-none-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl

Download URL geistr-0.4.0-py3-none-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
Size 892.9 kB
Tags Linux glibc 2.27+ ARM64 Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
04a5eaeac14a0c7f83fdbd1cd24c7f4821d9ac91d7831ea2e1f0ff4eebadac8b
BLAKE2b-256 checksum
How to use checksums
59819bd363575c1b5d12b7e4706498e17db7f72c98bc03bcfb97d381dd88b817
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / geistr-0.4.0-py3-none-macosx_15_0_arm64.whl

Download URL geistr-0.4.0-py3-none-macosx_15_0_arm64.whl
Size 829.0 kB
Tags Python 3 macOS 15.0+ ARM64
SHA-256 checksum
How to use checksums
74dfbbcc54e9381752fa188a7c102c6223a3531cf7bf7616ceb9e3ab9b322e29
BLAKE2b-256 checksum
How to use checksums
da9bece080abf44e652bdbe6e303c0c183ecd8764e835189e2f424827b808736
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for geistr 0.4.0

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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page