Skip to main content

LLMRouter ๐Ÿš€

Intelligent routing, load balancing, and failover for LLM providers

Python 3.10+ MIT License PyPI

LLMRouter is a Python package that acts as a routing layer between your application and LLM providers (OpenAI, Groq, Together AI, etc.). It handles provider selection, API key rotation, automatic failover, retries, and metricsโ€”so you don't have to.

# Instead of managing providers manually:
client = OpenAI(api_key="sk-...")
response = client.chat.completions.create(model="gpt-4", messages=[...])

# Use LLMRouter for intelligent routing:
router = LLMRouter(providers=[openai_provider, groq_provider])
response = await router.chat(prompt="Hello!")
# Router automatically picks the best provider, rotates keys, retries on failure, tracks metrics

โœจ Features

Core Capabilities (v0.1+)

  • ๐Ÿ”„ Automatic Provider Failover โ€” Switch to backup provider if primary fails
  • ๐Ÿ”‘ API Key Pooling โ€” Rotate across multiple keys to avoid rate limits
  • โฑ๏ธ Intelligent Scheduling โ€” Pick the best client (least busy, round-robin, random, weighted, priority)
  • ๐Ÿ” Automatic Retries โ€” Exponential backoff with jitter for transient failures
  • ๐Ÿ“Š Metrics Collection โ€” Track requests, errors, latency, throughput, success rates
  • ๐Ÿ”Œ Extensible Design โ€” Custom schedulers, retry policies, middleware
  • ๐Ÿ” Health Checks โ€” Monitor provider availability in real-time
  • ๐Ÿ“ก Streaming Support โ€” Stream responses from any provider
  • ๐Ÿ›ก๏ธ Error Handling โ€” Graceful degradation with meaningful exceptions

Planned Features (v0.2+)

  • ๐Ÿ’ฐ Cost-aware routing (pick cheapest provider)
  • โšก Latency-aware routing (pick fastest provider)
  • ๐ŸŒ Region-aware routing
  • ๐Ÿ’พ Response caching
  • ๐Ÿ” Authentication gateway
  • ๐Ÿ“‰ Prometheus metrics export

๐Ÿ“ฆ Installation

From PyPI

pip install llmrouterx

From Source (Development)

git clone https://github.com/amar8737/LLMRouter.git
cd LLMRouter
python3 -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -e .

Requirements

  • Python 3.10+ (tested on 3.10, 3.11, 3.12)
  • No external LLM SDKs required (bring your own: OpenAI, Groq, Together AI, etc.)

๐Ÿš€ Quick Start (5 Minutes)

1. Install the package

pip install llmrouterx

2. Basic example with OpenAI

import asyncio
from openai import AsyncOpenAI
from llmrouterx import LLMRouter
from llmrouterx.client import ClientNode
from llmrouterx.providers import ProviderRouter, CompositeRouter

async def main():
    # Create an OpenAI client
    client = AsyncOpenAI(api_key="sk-your-key-here")
    
    # Wrap it in a ClientNode (tracks identity and health)
    node = ClientNode("key-1", client)
    
    # Create a provider (represents a service like OpenAI)
    provider = ProviderRouter("openai", [node])
    
    # Create a composite router (aggregates providers)
    composite = CompositeRouter([provider])
    
    # Create the router
    router = LLMRouter(composite)
    
    # Make a request
    response = await router.chat(prompt="Hello, what's 2+2?")
    print(response)

if __name__ == "__main__":
    asyncio.run(main())

3. Run tests to verify installation

pip install pytest pytest-asyncio
pytest tests/ -v

๐Ÿ’ก Usage Examples

Example 1: Single Provider with One Key

import asyncio
from openai import AsyncOpenAI
from llmrouterx import LLMRouter
from llmrouterx.client import ClientNode
from llmrouterx.providers import ProviderRouter, CompositeRouter

async def main():
    client = AsyncOpenAI(api_key="sk-...")
    node = ClientNode("sk-...", client)
    provider = ProviderRouter("openai", [node])
    composite = CompositeRouter([provider])
    router = LLMRouter(composite)
    
    response = await router.chat(prompt="Hello!")
    print(response)

asyncio.run(main())

Example 2: Multiple API Keys (Rate Limit Protection)

import asyncio
from openai import AsyncOpenAI
from llmrouterx import LLMRouter
from llmrouterx.client import ClientNode
from llmrouterx.providers import ProviderRouter, CompositeRouter
from llmrouterx.scheduler import LeastBusyScheduler

async def main():
    keys = ["sk-key1", "sk-key2", "sk-key3"]
    nodes = [ClientNode(key, AsyncOpenAI(api_key=key)) for key in keys]
    
    # LeastBusyScheduler rotates across keys, avoiding rate limits
    provider = ProviderRouter(
        "openai",
        nodes,
        scheduler=LeastBusyScheduler()
    )
    composite = CompositeRouter([provider])
    router = LLMRouter(composite)
    
    response = await router.chat(prompt="Hello!")
    print(response)

asyncio.run(main())

Example 3: Multiple Providers with Automatic Failover

import asyncio
from openai import AsyncOpenAI
from groq import AsyncGroq
from llmrouterx import LLMRouter
from llmrouterx.client import ClientNode
from llmrouterx.providers import ProviderRouter, CompositeRouter
from llmrouterx.scheduler import LeastBusyScheduler

async def main():
    # Primary: OpenAI
    openai_node = ClientNode("sk-...", AsyncOpenAI(api_key="sk-..."))
    openai_provider = ProviderRouter("openai", [openai_node], scheduler=LeastBusyScheduler())
    
    # Fallback: Groq
    groq_node = ClientNode("gsk-...", AsyncGroq(api_key="gsk-..."))
    groq_provider = ProviderRouter("groq", [groq_node], scheduler=LeastBusyScheduler())
    
    # CompositeRouter tries providers in order; if first fails, tries next
    composite = CompositeRouter([openai_provider, groq_provider])
    router = LLMRouter(composite)
    
    # If OpenAI fails, automatically falls back to Groq
    response = await router.chat(prompt="Hello!")
    print(response)

asyncio.run(main())

Example 4: Streaming Responses

import asyncio
from llmrouterx import LLMRouter

async def main():
    router = LLMRouter(composite)  # from previous examples
    
    # Stream response chunks
    async for chunk in router.stream(prompt="Tell me a story"):
        print(chunk, end="", flush=True)
    print()

asyncio.run(main())

Example 5: Concurrent Requests

import asyncio
from llmrouterx import LLMRouter

async def main():
    router = LLMRouter(composite)
    
    # Make 10 concurrent requests
    prompts = [f"Request {i}: tell me a fact" for i in range(10)]
    responses = await asyncio.gather(*[
        router.chat(prompt=p) for p in prompts
    ])
    
    print(f"Got {len(responses)} responses")
    for i, resp in enumerate(responses):
        print(f"{i}: {resp[:100]}...")

asyncio.run(main())

Example 6: Custom Retry Policy

from llmrouterx.retry import ExponentialRetry
from llmrouterx import LLMRouter

# Retry with exponential backoff
retry = ExponentialRetry(
    max_retries=5,       # Try up to 5 times
    base=0.5,            # Start with 0.5 second wait
    factor=2.0,          # Double wait time each retry
    max_backoff=30.0     # Cap wait at 30 seconds
)

router = LLMRouter(composite, retry=retry)

Example 7: Monitor Metrics

import asyncio
from llmrouterx import LLMRouter

async def main():
    router = LLMRouter(composite)
    
    # Make some requests
    for i in range(10):
        await router.chat(prompt=f"Request {i}")
    
    # View metrics
    metrics = router.metrics.get()
    print("Total requests:", metrics["counters"].get("total_requests", 0))
    print("Total errors:", metrics["counters"].get("total_errors", 0))
    print("Average latency:", sum(metrics["timings"]) / len(metrics["timings"]) if metrics["timings"] else 0)

asyncio.run(main())

Example 8: Custom Middleware for Logging

import logging
from llmrouterx.middleware import BaseMiddleware
from llmrouterx import LLMRouter

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LoggingMiddleware(BaseMiddleware):
    async def before_request(self, op, payload):
        logger.info(f"โ†’ {op}: {payload}")
        return payload
    
    async def after_response(self, op, payload, response):
        logger.info(f"โ† {op}: response received")
        return response

# Use it
router = LLMRouter(composite, middleware=[LoggingMiddleware()])

Example 9: All Available Schedulers

from llmrouterx.scheduler import (
    LeastBusyScheduler,      # Pick client with fewest active requests
    RoundRobinScheduler,     # Rotate through clients sequentially
    RandomScheduler,         # Pick random client
    WeightedScheduler,       # Pick by node.weight property
    PriorityScheduler,       # Pick by node.priority property
)

# Use any scheduler
provider = ProviderRouter(
    "name",
    nodes,
    scheduler=LeastBusyScheduler()  # or any scheduler above
)

Example 10: Check Provider Health

async def check_health():
    composite = CompositeRouter([provider1, provider2])
    
    for provider in composite.providers:
        is_healthy = await provider.is_healthy()
        status = "โœ“ Up" if is_healthy else "โœ— Down"
        print(f"{provider.name}: {status}")

Example 11: Error Handling

import asyncio
from llmrouterx import LLMRouter
from llmrouterx.exceptions import NoHealthyClientError

async def main():
    router = LLMRouter(composite)
    
    try:
        response = await router.chat(prompt="Hello!")
    except NoHealthyClientError as e:
        print(f"All providers are down: {e}")
    except Exception as e:
        print(f"Request failed: {e}")

asyncio.run(main())

๐Ÿ”‘ Core Concepts

ClientNode

Represents a single LLM client (e.g., one OpenAI API key). Tracks health, active requests, and metadata.

node = ClientNode("identifier", client_instance)
node.weight = 2  # Optional: used by WeightedScheduler
node.priority = 10  # Optional: used by PriorityScheduler

ProviderRouter

Aggregates multiple ClientNodes for a single provider (e.g., OpenAI with 3 keys). Selects the best node using a scheduler.

provider = ProviderRouter(
    "openai",  # Provider name
    [node1, node2, node3],  # List of nodes
    scheduler=LeastBusyScheduler()  # How to select among nodes
)

CompositeRouter

Aggregates multiple ProviderRouters. Tries providers in order; if primary fails, tries next.

composite = CompositeRouter([openai_provider, groq_provider, fallback_provider])

LLMRouter

Main router. Orchestrates composite router, retry logic, middleware, and metrics.

router = LLMRouter(
    composite_router,
    retry=ExponentialRetry(),
    middleware=[LoggingMiddleware()],
)

๐Ÿ“Š Metrics

Track performance across your LLM infrastructure:

metrics = router.metrics.get()

# Counters
print(metrics["counters"]["total_requests"])
print(metrics["counters"]["total_errors"])
print(metrics["counters"]["total_successes"])

# Timings (list of request latencies in seconds)
print(metrics["timings"])  # [0.45, 0.52, 0.38, ...]

๐Ÿ”Œ Extensibility

Custom Scheduler

from llmrouterx.scheduler import BaseScheduler

class MyScheduler(BaseScheduler):
    async def select(self, provider_router):
        candidates = [c for c in provider_router.clients if await c.is_healthy()]
        if not candidates:
            return None
        # Your logic here
        return candidates[0]

provider = ProviderRouter("name", nodes, scheduler=MyScheduler())

Custom Middleware

from llmrouterx.middleware import BaseMiddleware

class MyMiddleware(BaseMiddleware):
    async def before_request(self, op, payload):
        # Modify request before sending
        return payload
    
    async def after_response(self, op, payload, response):
        # Transform response after receiving
        return response

router = LLMRouter(composite, middleware=[MyMiddleware()])

Custom Retry Policy

from llmrouterx.retry import BaseRetry

class MyRetry(BaseRetry):
    async def should_retry(self, error, attempt):
        # Your logic to decide if we should retry
        return attempt < 3

router = LLMRouter(composite, retry=MyRetry())

๐Ÿงช Testing

Run All Tests

pytest tests/ -v

Run Specific Test

pytest tests/test_streaming.py -v

Run Smoke Test

python tests/run_tests.py

Write Your Own Test

import pytest
from llmrouterx import LLMRouter
from llmrouterx.providers import StubClient, ProviderRouter, CompositeRouter
from llmrouterx.client import ClientNode

@pytest.mark.asyncio
async def test_basic_chat():
    stub = StubClient("test")
    node = ClientNode("test", stub)
    provider = ProviderRouter("test", [node])
    composite = CompositeRouter([provider])
    router = LLMRouter(composite)
    
    response = await router.chat(prompt="Hello")
    assert response is not None

๐Ÿ”ง Configuration

Minimal Setup

from llmrouterx import LLMRouter
from llmrouterx.providers import CompositeRouter, ProviderRouter
from llmrouterx.client import ClientNode

node = ClientNode("key", client)
provider = ProviderRouter("openai", [node])
composite = CompositeRouter([provider])
router = LLMRouter(composite)

Full Setup with All Options

from llmrouterx import LLMRouter
from llmrouterx.retry import ExponentialRetry
from llmrouterx.middleware import BaseMiddleware

class LogMiddleware(BaseMiddleware):
    async def before_request(self, op, payload):
        print(f"Sending: {op}")
        return payload

retry = ExponentialRetry(max_retries=5, base=1.0, factor=2.0)

router = LLMRouter(
    composite,
    retry=retry,
    middleware=[LogMiddleware()],
    # Additional config options as needed
)

๐Ÿ› Troubleshooting

Problem Solution
"No healthy providers" Check if providers are up, verify API keys are valid
Requests are slow Use LeastBusyScheduler, check metrics for latency outliers
Same key always used Check that scheduler is set to RoundRobin or LeastBusy
Errors not retrying Check should_retry() logic in retry policy, some errors are permanent
ModuleNotFoundError: llmrouterx Run pip install -e . if developing from source
Tests hang Update setuptools>=45 and run pip install -e . again

๐Ÿ“š API Reference

LLMRouter

class LLMRouter:
    def __init__(self, composite, retry=None, middleware=None):
        """Initialize router with composite, retry policy, and middleware."""
    
    async def chat(self, prompt: str, **kwargs) -> str:
        """Send chat request to best available provider."""
    
    async def stream(self, prompt: str, **kwargs):
        """Stream response chunks from best available provider."""
    
    async def embeddings(self, text: str, **kwargs) -> list:
        """Get embeddings from best available provider."""
    
    def get_metrics(self) -> dict:
        """Return metrics (counters and timings)."""

CompositeRouter

class CompositeRouter:
    def __init__(self, providers: list):
        """Initialize with list of ProviderRouters."""
    
    async def select(self) -> ProviderRouter:
        """Select best provider, trying in order."""
    
    async def is_healthy(self) -> bool:
        """Check if any provider is healthy."""

ProviderRouter

class ProviderRouter:
    def __init__(self, name: str, clients: list, scheduler=None):
        """Initialize provider with name, clients, and scheduler."""
    
    async def select_client(self) -> ClientNode:
        """Select best client using scheduler."""
    
    async def is_healthy(self) -> bool:
        """Check if any client is healthy."""

ClientNode

class ClientNode:
    def __init__(self, identifier: str, client):
        """Initialize with identifier and client instance."""
    
    async def is_healthy(self) -> bool:
        """Check if client is healthy."""
    
    def increment_active(self) -> None:
        """Increment active request count."""
    
    def decrement_active(self) -> None:
        """Decrement active request count."""

๐Ÿค Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Write tests for your changes
  4. Run tests: pytest tests/ -v
  5. Commit with clear messages: git commit -m "feat: add my feature"
  6. Push to your fork: git push origin feature/my-feature
  7. Open a PR with a clear description

Development Checklist

  • Code follows style guidelines (use black or autopep8)
  • Tests pass (pytest tests/ -v)
  • New features include tests
  • Documentation is updated
  • Commit messages are descriptive

Areas for Contribution

  • โœ… Cost-aware routing
  • โœ… Latency-aware routing
  • โœ… Response caching
  • โœ… Prometheus metrics export
  • โœ… Additional provider support
  • โœ… Performance optimizations
  • โœ… Documentation and examples

๐Ÿ“„ License

MIT License โ€” see LICENSE for details.


๐Ÿ™ Acknowledgments

Built with โค๏ธ for developers managing multiple LLM providers. Special thanks to the open-source community.


๐Ÿ“ž Support


๐Ÿš€ Roadmap

v0.1 (Current) โœ…

  • Basic routing and failover
  • API key pooling
  • Schedulers (round-robin, least-busy, random, weighted, priority)
  • Retry logic
  • Metrics collection

v0.2 (Planned)

  • Cost-aware routing
  • Latency-aware routing
  • Response caching
  • Prometheus metrics export

v0.3 (Future)

  • Region-aware routing
  • Batch request optimization
  • Advanced analytics dashboard

Ready to get started? Check out the Quick Start section or explore the examples above!

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llmrouterx-0.1.7.tar.gz (30.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llmrouterx-0.1.7-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

Details for the file llmrouterx-0.1.7.tar.gz.

File metadata

  • Download URL: llmrouterx-0.1.7.tar.gz
  • Upload date:
  • Size: 30.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for llmrouterx-0.1.7.tar.gz
Algorithm Hash digest
SHA256 8f690825c718e089cbb7022ded20d78fc245296aa267f0c07119728f756e8655
MD5 a6fc2b1af94d85dec95be0a211b57cd3
BLAKE2b-256 6191bcbb0c5f08fec0d7d9745e7732b7b18ec00a0cb9fe51a17667c2f3f07e75

See more details on using hashes here.

File details

Details for the file llmrouterx-0.1.7-py3-none-any.whl.

File metadata

  • Download URL: llmrouterx-0.1.7-py3-none-any.whl
  • Upload date:
  • Size: 28.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for llmrouterx-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 50c2c6886f09ea470a7a4b921e0a6636bb0fbdeda0a3b8e96b21c8ad47018d61
MD5 fe6d8332f8a5230790bce2db34f47167
BLAKE2b-256 d37adbd1b8340078f0a6876eafacadd73138acbbdb628d8f9eccd50bc7e90d9e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page