llm-pycascade
Resilient cascading LLM inference with automatic failover, circuit breaking, and retry cooldowns.
A faithful Python port of the llm-cascade Rust crate.
Features
- Cascade inference — define ordered fallback chains across multiple LLM providers
- Automatic failover — if a provider fails, the next in the cascade is tried immediately
- Circuit breaking / cooldowns — providers that fail repeatedly are put on exponential backoff
- Retry-After support — respects HTTP 429
Retry-Afterheaders - Failed-prompt persistence — conversations that exhaust all providers are saved as timestamped JSON
- Multi-provider — OpenAI, Anthropic, Google Gemini, and Ollama out of the box
- OpenAI-compatible — works with vLLM, LiteLLM, Together AI, and any OpenAI-compatible endpoint
- Tool/function calling — first-class support across all providers
- Async — built on
httpxandaiosqlitefor non-blocking I/O - Keyring integration — optional
keyringpackage for secure API key storage
Architecture
┌──────────────┐ ┌──────────────────────────────────────────┐
│ User Code │────▶│ run_cascade() │
└──────────────┘ │ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Entry 1 │─▶│ Entry 2 │─▶│ Entry 3 │ │
│ │OpenAI │ │Anthropic│ │ Gemini │ │
│ │gpt-4o │ │claude.. │ │gemini.. │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Cooldown Tracker (SQLite) │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ is_on_cooldown() / set_cooldown│ │ │
│ │ └───────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Attempt Log (SQLite) │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ log_attempt(status, latency, │ │ │
│ │ │ tokens) │ │ │
│ │ └───────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ ▼ (all failed) │
│ ┌─────────────────────────────────────┐ │
│ │ save_failed_conversation() → JSON │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘
Installation
pip
pip install llm-pycascade
uv
uv add llm-pycascade
With optional keyring support
pip install llm-pycascade[keyring]
Configuration
llm-pycascade can be configured from a TOML file (defaults to
~/.config/llm-pycascade/config.toml) or directly from a Python dict —
useful on stateless machines where no config file can exist:
from llm_pycascade import config_from_dict
config = config_from_dict({
"providers": {
"openai": {"type": "openai", "api_key_env": "OPENAI_API_KEY"},
"ollama": {"type": "ollama"},
},
"cascades": {
"primary": {
"entries": [
{"provider": "openai", "model": "gpt-4o"},
{"provider": "ollama", "model": "llama3.1"},
]
}
},
})
The dict uses the same schema as the TOML file (see
Configuration
for all options, including inline API keys via api_key_literal).
Equivalent TOML:
# config.example.toml — full example configuration
[providers.openai]
type = "openai"
api_key_env = "OPENAI_API_KEY"
[providers.anthropic]
type = "anthropic"
api_key_env = "ANTHROPIC_API_KEY"
[providers.gemini]
type = "gemini"
api_key_env = "GEMINI_API_KEY"
[providers.ollama]
type = "ollama"
# No API key needed for local Ollama
[cascades.primary]
entries = [
{ provider = "openai", model = "gpt-4o" },
{ provider = "anthropic", model = "claude-sonnet-4-20250514" },
{ provider = "gemini", model = "gemini-2.0-flash" },
{ provider = "ollama", model = "llama3.1" },
]
[cascades.fast]
entries = [
{ provider = "anthropic", model = "claude-haiku-4-20250507" },
{ provider = "openai", model = "gpt-4o-mini" },
{ provider = "ollama", model = "mistral" },
]
[database]
path = "~/.local/share/llm-pycascade/db.sqlite"
[failure_persistence]
dir = "~/.local/share/llm-pycascade/failed_prompts"
Usage
Basic cascade
import asyncio
from llm_pycascade import (
AppConfig,
Conversation,
init_db,
load_config,
run_cascade,
)
async def main():
config = load_config()
db_path = config.database.path.replace("~", "/home/user")
conn = await init_db(db_path)
conversation = Conversation.single_user_prompt("Explain quantum computing in one sentence.")
try:
response = await run_cascade("primary", conversation, config, conn)
print(response.text_only())
finally:
await conn.close()
asyncio.run(main())
With tools
from llm_pycascade import ContentBlock, Message, ToolDefinition
tools = [
ToolDefinition(
name="get_weather",
description="Get the current weather for a city",
parameters={
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
},
"required": ["city"],
},
)
]
conversation = Conversation.with_tools(
messages=[Message.user("What's the weather in Tokyo?")],
tools=tools,
)
response = await run_cascade("primary", conversation, config, conn)
for block in response.content:
if block.type == ContentBlockType.TEXT:
print(f"Text: {block.text}")
elif block.type == ContentBlockType.TOOL_CALL:
print(f"Tool call: {block.name}({block.arguments})")
Environment variable override
export LLM_PYCASCADE_CONFIG=/path/to/custom/config.toml
Key Types
| Type | Module | Description |
|---|---|---|
run_cascade() |
cascade |
Main entry point — runs a named cascade |
AppConfig |
config |
Top-level configuration (from TOML file or dict) |
config_from_dict() |
config |
Build an AppConfig from a plain Python dict |
load_config() |
config |
Load an AppConfig from a TOML file |
ProviderConfig |
config |
Per-provider settings (type, API key, base URL) |
CascadeConfig |
config |
Ordered list of provider/model entries |
CascadeEntry |
config |
Single provider/model pair in a cascade |
Conversation |
models |
Multi-turn conversation with optional tools |
Message |
models |
Single message (role + content) |
MessageRole |
models |
Enum: system, user, assistant, tool |
LlmResponse |
models |
Provider response with content blocks and usage |
ContentBlock |
models |
Discriminated union: text or tool_call |
ToolDefinition |
models |
Tool/function schema definition |
ProviderError |
error |
Provider-level failure (HTTP, parse, missing key) |
CascadeError |
error |
All-providers-exhausted failure |
Cooldown & Backoff
| Failure # | Cooldown Duration | Capped At |
|---|---|---|
| 1st | 60s | — |
| 2nd | 120s | — |
| 3rd | 240s | — |
| 4th | 480s | — |
| 5th | 960s | — |
| 6th+ | 1920s+ | 3600s (1h) |
Special cases:
- HTTP 429 with
Retry-After: uses the header value directly instead of exponential backoff - Cooldown check: the entry is silently skipped if still on cooldown
- Cooldown expiry: cooldowns are checked against real-time; expired entries are automatically available
License
MIT — see LICENSE.
Metadata
Release files for llm-pycascade 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| llm_pycascade-0.2.0.tar.gz | 86.6 kB | With provenance | Details for llm_pycascade-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| llm_pycascade-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for llm_pycascade-0.2.0-py3-none-any.whl |
Total release size: 116.6 kB
Release files / llm_pycascade-0.2.0.tar.gz
| Download URL | llm_pycascade-0.2.0.tar.gz |
|---|---|
| Size | 86.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
33c13c1a244405574f2326d42f36ee7c01815228146d3351ba5b5b300c806d76
|
|
BLAKE2b-256 checksum How to use checksums |
97f5073f6605c9b10f8826ba13d50cf1ee4e4941a0a31eaac982019830ab07a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.
Transparency logRelease files / llm_pycascade-0.2.0-py3-none-any.whl
| Download URL | llm_pycascade-0.2.0-py3-none-any.whl |
|---|---|
| Size | 30.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d44ddfeb45beeb4df9b54847d0793d1290219f0a2cf2e7663fdbb79187019f54
|
|
BLAKE2b-256 checksum How to use checksums |
75cd5c878e5f0be8ad4c49890a97dc53eedb815b27003a83e5aeff3312e404a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.
Transparency logSecurity for llm-pycascade 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for llm_pycascade-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.