Skip to main content

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 — ChatOptions lets you override model, temperature, max_tokens, and more at call time
  • Validated configuration — LLMClientConfig is 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)

Source distribution for unified-llm-provider 0.1.2
File Size Uploaded
unified_llm_provider-0.1.2.tar.gz 13.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for unified-llm-provider 0.1.2
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

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