A zen, simple, and unified API to prompt LLMs from Anthropic, Google, OpenAI, and more, using only the requests library.
Project description
🧘♂️ ZenLLM
The zen, simple, and unified API for LLMs with the best developer experience: two ergonomic entry points and one consistent return type.
Philosophy: No SDK bloat. Just requests and your API keys. Multimodal in and out. Streaming that’s easy to consume.
✨ What’s new (breaking change)
- Two functions: generate() for single-turn, chat() for multi-turn.
- Simple inputs for 95% cases. Escape hatch for advanced parts remains.
- Always returns a structured Response (or a ResponseStream when streaming).
- Image outputs are first-class (bytes or URLs), not lost in translation.
🚀 Installation
pip install zenllm
💡 Quick start
First, set your provider’s API key (e.g., export OPENAI_API_KEY="your-key").
You can also set a default model via environment:
- export ZENLLM_DEFAULT_MODEL="gpt-4.1"
Text-only
import zenllm as llm
resp = llm.generate("Why is the sky blue?", model="gpt-4.1")
print(resp.text)
Vision (single image shortcut)
import zenllm as llm
resp = llm.generate(
"What is in this photo?",
model="gemini-2.5-pro",
image="cheeseburger.jpg", # path, URL, bytes, or file-like accepted
)
print(resp.text)
Vision (image generation output)
Gemini can return image data inline. Save them with one call.
import zenllm as llm
resp = llm.generate(
"Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme",
model="gemini-2.5-flash-image-preview",
)
resp.save_images(prefix="banana_") # writes banana_0.png, ...
Multi-turn chat with shorthands
import zenllm as llm
resp = llm.chat(
[
("system", "Be concise."),
("user", "Describe this image in one sentence.", "cheeseburger.jpg"),
],
model="claude-sonnet-4-20250514",
)
print(resp.text)
Streaming with typed events
import zenllm as llm
stream = llm.generate(
"Generate an image and a short caption.",
model="gemini-2.5-flash-image-preview",
stream=True,
)
caption = []
for ev in stream:
if ev.type == "text":
caption.append(ev.text)
print(ev.text, end="", flush=True)
elif ev.type == "image":
if getattr(ev, "bytes", None):
with open("out.png", "wb") as f:
f.write(ev.bytes)
elif getattr(ev, "url", None):
print(f"\nImage available at: {ev.url}")
final = stream.finalize() # Response
Using OpenAI-compatible endpoints
Works with local or third-party OpenAI-compatible APIs by passing base_url.
import zenllm as llm
# Local model (e.g., Ollama or LM Studio)
resp = llm.generate(
"Why is the sky blue?",
model="qwen3:30b",
base_url="http://localhost:11434/v1",
)
print(resp.text)
# Streaming
stream = llm.generate(
"Tell me a story.",
model="qwen3:30b",
base_url="http://localhost:11434/v1",
stream=True,
)
for ev in stream:
if ev.type == "text":
print(ev.text, end="", flush=True)
🧱 API overview
- generate(prompt=None, *, model=..., system=None, image=None, images=None, stream=False, options=None, provider=None, base_url=None, api_key=None)
- chat(messages, *, model=..., system=None, stream=False, options=None, provider=None, base_url=None, api_key=None)
Inputs:
- prompt: str
- image: single image source (path, URL, bytes, file-like)
- images: list of image sources (same kinds)
- messages shorthands:
- "hello"
- ("user"|"assistant"|"system", text[, images])
- {"role":"user","text":"...", "images":[...]}
- {"role":"user","parts":[...]} // escape hatch for experts
- options: normalized tuning and passthrough, e.g. {"temperature": 0.7, "max_tokens": 512}. These are mapped per provider where needed.
Helpers (escape hatch):
- zenllm.text(value) -> {"type":"text","text": "..."}
- zenllm.image(source[, mime, detail]) -> {"type":"image","source":{"kind": "...","value": ...}, ...}
Outputs:
- Always a Response object with:
- response.text: concatenated text
- response.parts: normalized parts
- {"type":"text","text":"..."}
- {"type":"image","source":{"kind":"bytes"|"url","value":...},"mime":"image/png"}
- response.images: convenience filtered list
- response.finish_reason, response.usage, response.raw
- response.save_images(dir=".", prefix="img_")
- response.to_dict() for JSON-safe structure (bytes are base64, kind becomes "bytes_b64")
Streaming:
- Returns a ResponseStream. Iterate events:
- Text events: ev.type == "text", ev.text
- Image events: ev.type == "image", either ev.bytes (with ev.mime) or ev.url
- Call stream.finalize() to materialize a Response from the streamed events.
Provider selection:
- Automatic by model prefix: gpt, gemini, claude, deepseek, together
- Override with provider="gpt"|"openai"|"openai-compatible"|"gemini"|"claude"|"deepseek"|"together"
- OpenAI-compatible: pass base_url (and optional api_key) and we append /chat/completions
✅ Supported Providers
| Provider | Env Var | Prefix | Notes | Example Models |
|---|---|---|---|---|
| Anthropic | ANTHROPIC_API_KEY |
claude |
Text + Images (input via base64) | claude-sonnet-4-20250514, claude-opus-4-20250514 |
| DeepSeek | DEEPSEEK_API_KEY |
deepseek |
OpenAI-compatible; image support may vary | deepseek-chat, deepseek-reasoner |
GEMINI_API_KEY |
gemini |
Text + Images (inline_data base64) | gemini-2.5-pro, gemini-2.5-flash |
|
| OpenAI | OPENAI_API_KEY |
gpt |
Text + Images (image_url, supports data URLs) |
gpt-4.1, gpt-4o |
| TogetherAI | TOGETHER_API_KEY |
together |
OpenAI-compatible; image support may vary | together/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo |
Notes:
- For OpenAI-compatible endpoints (like local models), pass
base_urland optionalapi_key. We’ll route via the OpenAI-compatible provider and append/chat/completions. - Some third-party endpoints don’t support vision. If you pass images to an unsupported model, the upstream provider may return an error.
- DeepSeek and Together may not accept image URLs; prefer path/bytes/file for images with those providers.
🧪 Advanced examples
Manual parts with helpers:
from zenllm import text, image
import zenllm as llm
msgs = [
{"role": "user", "parts": [
text("Describe this in one sentence."),
image("cheeseburger.jpg", detail="high"),
]},
]
resp = llm.chat(msgs, model="gemini-2.5-pro")
print(resp.text)
Provider override:
import zenllm as llm
resp = llm.generate(
"Hello!",
model="gpt-4.1",
provider="openai", # or "gpt", "openai-compatible", "gemini", "claude", "deepseek", "together"
)
print(resp.text)
Serialization:
d = resp.to_dict() # bytes are base64-encoded with kind "bytes_b64"
📜 License
MIT License — Copyright (c) 2025 Koen van Eijk
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zenllm-0.2.0.tar.gz.
File metadata
- Download URL: zenllm-0.2.0.tar.gz
- Upload date:
- Size: 15.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fcb9195ca615764ea3f4da142d5ff28b6229888dd56bac383c196de9afdd836
|
|
| MD5 |
b6989a0b4f4cbd42d32aac06ef468aa3
|
|
| BLAKE2b-256 |
92e67b050380eea07c50c9f4054df531e5c65baca5a8fc968580d8c7a45c7560
|
Provenance
The following attestation bundles were made for zenllm-0.2.0.tar.gz:
Publisher:
publish_to_pypi.yml on koenvaneijk/zenllm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zenllm-0.2.0.tar.gz -
Subject digest:
2fcb9195ca615764ea3f4da142d5ff28b6229888dd56bac383c196de9afdd836 - Sigstore transparency entry: 472879242
- Sigstore integration time:
-
Permalink:
koenvaneijk/zenllm@84ae1239261f9814b281bc154f6d85e7f67b0c7f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/koenvaneijk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_to_pypi.yml@84ae1239261f9814b281bc154f6d85e7f67b0c7f -
Trigger Event:
release
-
Statement type:
File details
Details for the file zenllm-0.2.0-py3-none-any.whl.
File metadata
- Download URL: zenllm-0.2.0-py3-none-any.whl
- Upload date:
- Size: 19.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dee8443252994eac5fa7ad71386c0a0f3d2b7c90a9a34b37447202b22fa168bd
|
|
| MD5 |
497fc8ea347c4998e8636a1471d35789
|
|
| BLAKE2b-256 |
e3077fa41d277b3a8e4e4d9c42b0952b2f51fb600f2e49e43f61f80f9f65c003
|
Provenance
The following attestation bundles were made for zenllm-0.2.0-py3-none-any.whl:
Publisher:
publish_to_pypi.yml on koenvaneijk/zenllm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zenllm-0.2.0-py3-none-any.whl -
Subject digest:
dee8443252994eac5fa7ad71386c0a0f3d2b7c90a9a34b37447202b22fa168bd - Sigstore transparency entry: 472879265
- Sigstore integration time:
-
Permalink:
koenvaneijk/zenllm@84ae1239261f9814b281bc154f6d85e7f67b0c7f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/koenvaneijk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_to_pypi.yml@84ae1239261f9814b281bc154f6d85e7f67b0c7f -
Trigger Event:
release
-
Statement type: