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.

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 (a release that is not on the server is reported as "no release …"; releases before 0.3.2 never were):

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.3.2 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: suggests a model for this computer,
                               # downloads and verifies it on Enter, then chats

Or choose yourself:

geistr catalog                 # which models fit this computer
geistr pull qwen3-0.6b         # download and verify one (640 MB)
geistr chat qwen3-0.6b         # talk to it

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). 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.

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
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.

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 one 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, 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). --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, put a one-line task before a query (Instruct: Given a question, retrieve passages that answer the question\nQuery: …), nothing before documents. An embedding model does not chat, and a chat model does not embed (400). Not supported yet: more than one model per service.

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): a larger one is refused at once (HTTP 413). The socket protocol is one JSON object per line, documented in tools/geistr/service.h:

echo '{"op":"chat","messages":[{"role":"user","content":"Hi"}]}' | nc -U ~/…/geistr.sock

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 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.

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

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

Metadata

Release files for geistr 0.3.3

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.3.3
File Interpreter ABI Platform Actions
geistr-0.3.3-py3-none-manylinux_2_39_aarch64.whl Python 3 none Linux glibc 2.39+ ARM64 Details for geistr-0.3.3-py3-none-manylinux_2_39_aarch64.whl
geistr-0.3.3-py3-none-manylinux_2_38_x86_64.whl Python 3 none Linux glibc 2.38+ x86-64 Details for geistr-0.3.3-py3-none-manylinux_2_38_x86_64.whl
geistr-0.3.3-py3-none-macosx_15_0_arm64.whl Python 3 none macOS 15.0+ ARM64 Details for geistr-0.3.3-py3-none-macosx_15_0_arm64.whl

Total release size: 2.7 MB

Release files / geistr-0.3.3-py3-none-manylinux_2_39_aarch64.whl

Download URL geistr-0.3.3-py3-none-manylinux_2_39_aarch64.whl
Size 846.1 kB
Tags Linux glibc 2.39+ ARM64 Python 3
SHA-256 checksum
How to use checksums
b73b11191688c28108c19665e1f047e0c52a85aa8845115367d07a577430e7a9
BLAKE2b-256 checksum
How to use checksums
33c04b272b06b51f924d536a03f9e28ccd3963b6b9d7e8ded9f12f627ad51b5b
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.3.3-py3-none-manylinux_2_38_x86_64.whl

Download URL geistr-0.3.3-py3-none-manylinux_2_38_x86_64.whl
Size 1.1 MB
Tags Linux glibc 2.38+ x86-64 Python 3
SHA-256 checksum
How to use checksums
786e04314d9986f99167602f0353a828d890f5bd06d3b47dc41afeb9acb5c360
BLAKE2b-256 checksum
How to use checksums
58129fd4c4e4970961fe74fa683240de6383f58823cec2b7bd4abd205e17d969
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.3.3-py3-none-macosx_15_0_arm64.whl

Download URL geistr-0.3.3-py3-none-macosx_15_0_arm64.whl
Size 730.6 kB
Tags Python 3 macOS 15.0+ ARM64
SHA-256 checksum
How to use checksums
5ba02993ea6b27f39cda1f0190acc3e426dff0ddc09b77e02ab601392df4680d
BLAKE2b-256 checksum
How to use checksums
00b8feeb8f6356fbdc08aa81cea9def65317fd277457bda8108dbbe17f2b91c2
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.3.3

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