Python SDK

Keep your AI project running when free APIs hit their limits.

LLMSwitch sits between your code and the free tiers of eight AI providers. Write one call. When a provider rate-limits you, it fails over to the next one — and remembers not to keep knocking on a door that just closed.

version 0.4.0 python 3.9+ ci green 3.9–3.13 license MIT tests 150 passing
01 — Quick start

Two minutes to a working fallback chain

pip install ai-switch

The distribution is named ai-switch — PyPI rejected llmswitch as too similar to an unrelated existing project. The import name is unchanged.

Set a key for any provider you want in the rotation — LLMSwitch only uses the ones it finds:

export GROQ_API_KEY="..."
export OPENROUTER_API_KEY="..."
export NVIDIA_API_KEY="..."

Then write the call once:

from llmswitch import AIClient

client = AIClient()
print(client.chat("Write a Python web scraper"))

Your code never names a provider. That is the whole idea.

02 — Configuration

Keys and provider order

ProviderEnvironment variableOrder
groqGROQ_API_KEY1
cerebrasCEREBRAS_API_KEY2
geminiGEMINI_API_KEY3
openrouterOPENROUTER_API_KEY4
nvidiaNVIDIA_API_KEY5
mistralMISTRAL_API_KEY6
huggingfaceHF_TOKEN7
ollamaOLLAMA_HOST8 — local last resort

Providers without a key are skipped silently at startup.

Ollama as a local last resort

Ollama needs no API key and never rate-limits, which makes it a natural final fallback once every cloud free tier is spent. It is opt-in: set OLLAMA_HOST to enable it, so anyone without a local server does not pay for a failed attempt on every request.

export OLLAMA_HOST="http://localhost:11434/v1"

To pick the set and the order yourself:

from llmswitch import AIClient
from llmswitch.providers import GroqProvider, NVIDIAProvider

client = AIClient(providers=[GroqProvider(), NVIDIAProvider()])
03 — How routing works

One request, as many attempts as it takes

A single chat() call walks the provider list until one answers:

your app
client.chat("explain machine learning")
groq
HTTP 429 — daily quota exhausted, rest 60s
openrouter
connection refused — rest 10s
nvidia
200 — response returned to your app

Two failure kinds drive this. RateLimitError means you have been throttled or have burned the quota. ProviderUnavailableError means the provider is down, unreachable, or rejected the request. Anything else propagates untouched — a bug in your payload should not silently cost you three API calls.

If every provider fails, LLMSwitch raises ProviderError naming each failure, so you can see what actually went wrong.

04 — Health tracking

It remembers which doors just closed

Falling back on every request is not enough. Without memory, a rate-limited Groq is called again on the next request, and the one after that — burning a wasted round-trip every time before failing over. LLMSwitch rests a failed provider and moves it to the back of the queue:

client.chat("first")   # groq 429 -> falls back to openrouter
client.chat("second")  # groq skipped entirely; openrouter serves it

Inspect the live picture at any time with client.status():

provider health
groq 57s left cooling
openrouter 12 ok / 0 fail available
nvidia 3 ok / 1 fail available

How long a provider rests

  • If the provider sent a Retry-After header, that wins — it knows better than any guess.
  • Otherwise rate limits back off exponentially from 60s, doubling per consecutive failure, capped at 15 minutes.
  • Transient outages start at 10s and cap at 5 minutes, since they usually clear fast.
  • One success resets the streak, so a brief blip does not poison a provider for the rest of the session.
Cooldowns are advisory, not a lockout. If every provider is resting, LLMSwitch still tries them — closest-to-recovery first — rather than failing without an attempt. A degraded answer beats a dead app.
05 — Model aliases

Stop memorizing provider-specific model names

The same model is named three different things by three providers. Ask for the alias and LLMSwitch resolves it per provider:

AliasGroqOpenRouterNVIDIA
fast-model llama-3.1-8b-instant meta-llama/llama-3.1-8b-instruct meta/llama-3.1-8b-instruct
smart-model llama-3.3-70b-versatile meta-llama/llama-3.3-70b-instruct meta/llama-3.3-70b-instruct
client.chat("hello", model="fast-model")        # resolved per provider
client.chat("hello", model="llama-3.1-8b-instant") # raw names pass through
Provider catalogs churn. fast-model is verified against live Groq and OpenRouter APIs; smart-model is not yet verified — check the current catalog before relying on it.
06 — Streaming & async

Tokens as they arrive, on either transport

with AIClient() as client:
    for token in client.stream("Write a haiku about failover"):
        print(token, end="", flush=True)

What happens when a stream breaks

Fallback is transparent only up to the first token. Once tokens are in your hands they cannot be un-yielded, so a later failure raises StreamInterrupted carrying the partial text — rather than silently restarting the answer on another provider and making your reader watch it begin again mid-sentence.

before token 1
provider swapped silently — the caller never knows
after token 1
StreamInterrupted(partial=…, provider=…) — you decide what to do
except StreamInterrupted as exc:
    print(exc.partial)   # everything received before the break
    print(exc.provider)  # which provider dropped it

Async

AsyncAIClient shares the sync client's routing core, so fallback, cooldowns and retry behave identically — only the transport differs.

async with AsyncAIClient() as client:
    print(await client.chat("hello"))
    async for token in client.stream("count to five"):
        print(token, end="")

Connections

Each provider holds a pooled connection, reused across calls. Close the client when done — the context manager above is the tidy path; forgetting it still works, just cleaned up later.

Retries

A timeout usually means one unlucky request rather than a dead provider, so the same provider gets one retry before failover. A 429 is never retried — the provider has explicitly said stop.

07 — API reference

The whole surface

AIClient(providers=None, health=None)

Both arguments are injectable, which is what makes the router testable without network access.

MemberReturnsNotes
chat(message, model="fast-model", **kwargs) str Accepts a string or an OpenAI-style messages list. Extra kwargs (temperature, max_tokens) forward to the provider.
stream(message, model="fast-model", **kwargs) Iterator[str] Yields tokens. Raises StreamInterrupted if a provider dies after emitting.
close() / await aclose() None Release pooled connections. Both clients are context managers.
status() dict Availability, cooldown remaining, success/failure counts, last error.
providerslistThe resolved provider instances.
healthHealthTrackerShareable across clients.

Exceptions

TypeRaised when
ProviderErrorBase type; also raised when every provider has failed.
RateLimitErrorHTTP 429 or quota exhausted. Carries .retry_after.
ProviderUnavailableErrorConnection failure, 5xx, or a rejected request.
ProviderTimeoutErrorRequest timed out. Subclasses the above; earns one retry.
StreamInterruptedStream failed after emitting. Carries .partial and .provider.
08 — Extending

Writing an adapter

Any provider speaking the OpenAI chat-completions shape needs three lines:

from llmswitch.providers.base import OpenAICompatibleProvider

class MyProvider(OpenAICompatibleProvider):
    name = "myprovider"
    api_base = "https://api.example.com/v1"
    api_key_env = "MYPROVIDER_API_KEY"

The base class handles auth headers, the User-Agent, Retry-After parsing, and mapping status codes onto the two error types. For a provider with its own wire format (Gemini, for instance), subclass Provider and implement chat() directly — just raise the same two exceptions so the router can do its job.

Groq sits behind Cloudflare and blocks generic client signatures — a bare Python-urllib request gets a 403. LLMSwitch sends an explicit User-Agent for this reason. Worth knowing if you write your own adapter.
09 — Roadmap

What is built and what is next

v0.1shipped
Provider interface, Groq / OpenRouter / NVIDIA adapters, sequential fallback, model aliases, 33 tests.
v0.2shipped
Health tracking with cooldowns, exponential backoff, Retry-After support, and status(). Published to PyPI. Suite now 64 tests.
v0.3shipped
Five more providers — Gemini, Hugging Face, Cerebras, Mistral, and a keyless local Ollama fallback. Adapters can now run without an API key and take a custom host. Suite now 113 tests.
v0.4shipped
Streaming, AsyncAIClient, pooled connections, retry-on-timeout, and CI green across Python 3.9–3.13 against real httpx. Suite now 150 tests.
v0.5next
llmswitch.toml config, routing strategies beyond in-order (fastest-first, round-robin, quota-aware), and a script validating model aliases against providers' live catalogs.