A tiny, unified client for four LLM providers: claude, openai, gemini, ollama.
Project description
thinchat
English | 한국어
A tiny, unified client for four LLM providers — claude, openai, gemini, ollama.
Name a provider, then call it. Every client offers completion — whole, streamed, or JSON-structured — and, where the provider has one, embeddings, each with an async twin. No gateway, no router, no cost tracking: just the calls, over the providers' own SDKs.
How it works
flowchart LR
M["make_client(provider)"] --> C["Client<br/>openai-compatible · or claude"]
C --> V["complete · stream · parse · embed<br/>(+ a-prefixed async twins)"]
V --> S{{"vendor SDK"}}
S -->|ok| O(["str · dict · list float · stream"])
S -->|"SDK / transport error"| E(["LLMError"])
make_client looks the provider up in one factory map: openai/gemini/ollama share a single
class over the openai SDK (they differ only in data); claude has its own over anthropic.
A call builds the request, hits the SDK, and either extracts the reply or maps the failure
to one LLMError.
Install
pip install thinchat
Both provider SDKs (openai and anthropic) come with it, so every provider works out of the box; each is imported lazily the first time you construct its client.
Use
from thinchat import make_client
llm = make_client("claude") # key from CLAUDE_API_KEY
print(llm.complete("Say hi in one word."))
print(llm.complete("Name a color.", system="Answer in one word.")) # system= steers any verb
# Structured output: reply parsed into a JSON object. The schema steers generation
# but is not validated locally, so check the returned dict's fields yourself.
verdict = llm.parse(
"Is this an ad? 'Buy now, 50% off — order today'",
schema={"type": "object",
"properties": {"is_ad": {"type": "boolean"}, "reason": {"type": "string"}},
"required": ["is_ad"]},
)
print(verdict["is_ad"])
# Streaming.
for chunk in make_client("claude").stream("Count to five."):
print(chunk, end="")
# Embeddings (openai / gemini / ollama; Claude has none).
vectors = make_client("openai").embed(["hello", "world"])
Every verb has an async twin — acomplete, astream, aparse, aembed:
llm = make_client("claude")
text = await llm.acomplete("Summarize in one line: ...")
Providers
| provider | key env | embeddings |
|---|---|---|
claude |
CLAUDE_API_KEY |
no |
openai |
OPENAI_API_KEY |
yes |
gemini |
GEMINI_API_KEY |
yes |
ollama |
none (local) | yes |
openai, gemini, and ollama speak the same OpenAI-compatible API, so one SDK serves all
three; only the base URL, key, and default models differ. Ollama runs locally
(OLLAMA_HOST, default http://localhost:11434) and needs no key.
max_tokens caps the reply length — make_client("claude", max_tokens=8192). Anthropic
requires the field, so claude defaults to 4096; the OpenAI-compatible providers omit it unless
you pass one, letting the model decide.
Keys are read from the environment. Set them once in your shell profile (~/.bashrc,
~/.zshrc) so every session picks them up — thinchat is a library and never imposes a file
location of its own:
export CLAUDE_API_KEY="sk-ant-..."
export OPENAI_API_KEY="sk-..."
export GEMINI_API_KEY="..." # ollama runs locally and needs no key
Or pass a key explicitly, which overrides the environment:
llm = make_client("claude", api_key="sk-ant-...", model="claude-haiku-4-5-20251001")
Capabilities
A client whose provider lacks a capability raises UnsupportedError. The capabilities are
completion, streaming, structured_output, and embeddings; check first with supports:
make_client("claude").supports("embeddings") # False
Errors
Everything thinchat raises on purpose derives from ThinchatError, so one except handles
the package's failures:
UnknownProviderError— the name isn't one of the four providers.ProviderUnavailableError— the provider's SDK isn't installed, or no API key is set.UnsupportedError— the provider lacks the capability (e.g. embeddings on Claude).LLMError— the API call failed, or the reply was empty or malformed.
Lifecycle
A client holds an HTTP connection pool. For a one-off script you can ignore it; for a
server that builds a client per request, close it so connections do not leak — use it as a
context manager, or call close() / aclose():
with make_client("claude") as llm:
llm.complete("...") # sync: closes the pool on exit
async with make_client("claude") as llm:
await llm.acomplete("...") # async: closes the async pool too
close() frees the sync pool; if you drove async verbs, release with aclose() or
async with so the async pool is closed as well.
License
MIT
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 thinchat-0.1.0.tar.gz.
File metadata
- Download URL: thinchat-0.1.0.tar.gz
- Upload date:
- Size: 26.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e54af5c66d1933754b27fe06ee5fa5149d4becfa7737b9b9cce0d05e3af9cd7
|
|
| MD5 |
872e58e14ded3f15517ab8d1e131b9fb
|
|
| BLAKE2b-256 |
6eca22728ec7822c79d05291c5744794380d759a449dd48d915ea2e70ce2b42f
|
File details
Details for the file thinchat-0.1.0-py3-none-any.whl.
File metadata
- Download URL: thinchat-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c7238abb9be745d627da501810f64633cbff335511e420c1f9f92cf1bfe2850c
|
|
| MD5 |
317905c63f2cdb9a63315fd7feda43ed
|
|
| BLAKE2b-256 |
64eb95bfdf3ab5a34d2e117fef4cc41e8a9813fb42ed160382b2a1f2c98517fc
|