Unified LLM Provider
A Python library providing a single, unified async interface for multiple LLM providers — OpenAI, Ollama (local), and OpenRouter — so you can switch providers without changing your application code.
Features
- Unified API — one
LLMClient, three providers, identical method signatures - Event-based streaming — streams are emitted as typed dataclass events (
MessageStartEvent,TextDeltaEvent, etc.), not raw chunks - Reasoning / "thinking" support — reasoning models get dedicated start/delta/end events
- Multimodal (vision) — messages can contain text + images; local image files are auto base64-encoded
- Per-request overrides —
ChatOptionslets you override model, temperature, max_tokens, and more at call time - Validated configuration —
LLMClientConfigis a pydantic model with field/model validators
Requirements
- Python 3.12+
Installation
pip install -r requirements.txt
Quick Start
Basic chat (non-streaming)
import asyncio
from llm_client import LLMClient
from utils import LLMClientConfig
client = LLMClient(LLMClientConfig(
provider_id="openai",
api_key="sk-...",
model_name="gpt-4o-mini",
))
result = asyncio.run(client.get_response("Explain async Python in one paragraph."))
print(result)
Streaming with events
import asyncio
from llm_client import LLMClient
from utils import LLMClientConfig, ChatOptions
from Events.events import (
MessageStartEvent, TextDeltaEvent, MessageEndEvent, StreamEndEvent,
)
async def main():
client = LLMClient(LLMClientConfig(
provider_id="ollama",
model_name="ornith-1.5:9b",
))
async for event in client.chat_stream("Hello!"):
if isinstance(event, MessageStartEvent):
print("\n[stream started]")
elif isinstance(event, TextDeltaEvent):
print(event.content, end="")
elif isinstance(event, MessageEndEvent):
print(f"\n[done: {event.reason}]")
asyncio.run(main())
Passing the model per-request (optional init model)
The model is optional at init — you can provide it per-request via ChatOptions:
from utils import ChatOptions
client = LLMClient(LLMClientConfig(
provider_id="openai",
api_key="sk-...",
))
result = await client.chat(
"Write a haiku",
options=ChatOptions(model="gpt-4o-mini", temperature=0.2),
)
Vision (image) prompt
from utils import Message
messages = [Message(
role="user",
content=[
{"type": "text", "text": "What is this image about?"},
{"type": "image_url", "image_url": {"url": "photo.png"}},
]
)]
# Local file paths are auto base64-encoded.
# http(s) URLs and data: URLs pass through as-is.
List available models
models = await client.get_model()
API Reference
LLMClient
| Method | Returns | Description |
|---|---|---|
chat(prompt, options=None) |
Dict[str, Any] |
Non-streaming completion |
chat_stream(prompt, options=None) |
AsyncGenerator[Event] |
Streaming completion as typed events |
get_response(prompt, options=None) |
str |
Convenience: chat() + extract text content |
get_model() |
List[str] |
List available model IDs |
prompt can be a plain str or a List[Message].
LLMClientConfig (pydantic, validated)
| Field | Required | Default | Description |
|---|---|---|---|
provider_id |
No | "openai" |
"openai", "ollama", or "openrouter" |
endpoint |
No | Provider default | API base URL (must start with http:// or https://) |
api_key |
Yes (except Ollama) | — | Provider API key |
model_name |
No | — | Default model (can be overridden per-request) |
Provider defaults: Ollama → http://localhost:11434 (no key); OpenAI → https://api.openai.com/v1; OpenRouter → https://openrouter.ai/api/v1.
ChatOptions
Per-request overrides: model, temperature, max_tokens, top_p, frequency_penalty, presence_penalty, stop, n, seed, response_format.
Message
@dataclass
class Message:
role: Literal['user', 'assistant', 'system', 'function']
content: Any # str or multimodal list
Streaming Events
| Event | Payload | When |
|---|---|---|
MessageStartEvent |
— | Stream opens |
ReasoningStartEvent |
— | Model begins "thinking" |
ReasoningDeltaEvent |
content: str |
Reasoning chunk |
ReasoningEndEvent |
— | Reasoning finishes |
TextDeltaEvent |
content: str |
Answer text chunk |
MessageEndEvent |
reason: str |
Message complete |
StreamEndEvent |
— | Stream fully complete |
Supported Providers
| Provider | Transport | Notes |
|---|---|---|
| OpenAI | AsyncOpenAI SDK |
OpenAI-compatible /chat/completions |
| Ollama | httpx (native) | Native /api/chat; runs locally |
| OpenRouter | AsyncOpenAI SDK |
OpenAI-compatible |
Running the Tests
Tests load API keys from .env:
# OpenAI (needs OPENAI_API_KEY in .env)
python Test/test_openai.py
# OpenRouter (needs OPENROUTER_API_KEY in .env)
python Test/test_openrouter.py
# Ollama (needs local `ollama serve` + a vision-capable model)
python Test/test_ollama.py
Project Structure
Unified_LLM_Provider/
├── llm_client.py # LLMClient — main facade / public entry point
├── base_client.py # BaseClient — abstract base with shared chat & streaming logic
├── utils.py # LLMClientConfig, ChatOptions, Message, ModelConfig
├── contants.py # ProviderType enum, DefaultModelSettings
├── Provider/
│ ├── openai.py # OpenAIClient
│ ├── ollama.py # OllamaClient
│ └── openrouter.py # OpenRouterClient
├── Events/
│ └── events.py # Streaming event dataclasses
├── Test/
│ ├── test_openai.py
│ ├── test_ollama.py
│ └── test_openrouter.py
├── .env # API keys (not committed)
├── pyproject.toml
├── requirements.txt
└── README.md
Metadata
Release files for unified-llm-provider 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| unified_llm_provider-0.1.2.tar.gz | 13.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| unified_llm_provider-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 26.3 kB
Release files / unified_llm_provider-0.1.2.tar.gz
| Download URL | unified_llm_provider-0.1.2.tar.gz |
|---|---|
| Size | 13.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
08eb4dfa7fda12f34ab1cad30a515bda71da0d6d56c0728784ec14a77a743ef6
|
|
BLAKE2b-256 checksum How to use checksums |
8cefe955e36ebc29bd15e802d8aa7d54f661d130f982ea1b0f889501b5ea2e7c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / unified_llm_provider-0.1.2-py3-none-any.whl
| Download URL | unified_llm_provider-0.1.2-py3-none-any.whl |
|---|---|
| Size | 12.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
24f0a3a617c9b250da87744f0177a21df4453852a4d15c7073637eaba3a3f491
|
|
BLAKE2b-256 checksum How to use checksums |
68736bd36ca26bedd36b6382923f5626f805d32ef14ee24fe575a11307f5a99c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|