Self-healing unified LLM routing SDK for Anthropic + 350+ OpenRouter models
Project description
plaudapi
Standard wrappers fail when providers flake. plaudapi heals.
A unified Python SDK for Anthropic + 350+ OpenRouter models with dynamic routing, automatic failover, and heuristic self-repair. One interface. Zero babysitting.
Install
pip install plaudapi
plaudapi setup # saves API keys to ~/.plaudapi/.env
Quickstart
from plaudapi import PlaudClient
client = PlaudClient() # FREE_FIRST by default — no API key required to start
resp = client.chat([{"role": "user", "content": "Explain transformers in 2 sentences."}])
print(resp.content)
That's it. plaudapi picks the best available free model from the live OpenRouter catalog, falls back to a hardcoded default if the catalog is unreachable, and retries transient failures with full-jitter backoff — all without any extra wiring.
Why plaudapi?
| Raw SDK | plaudapi | |
|---|---|---|
| Multi-provider | One package per provider | Single unified interface |
| Free models | Manual catalog lookup | FREE_FIRST auto-selects from live catalog |
| Price optimization | Hardcode a model and hope | COST_OPTIMIZED sorts 350+ models by live pricing |
| Failover | Write your own try/except loop | Built-in jitter retry + strategy fallback |
| Malformed JSON | json.JSONDecodeError crashes |
Heuristic repair closes unclosed braces/quotes |
| Context overrun | Provider 400 error | Pre-flight token estimate raises PlaudContextWindowError |
| Auth errors | Confusing 401 response |
PlaudAuthError tells you exactly which env var is missing |
| Streaming | Different API per provider | client.stream() / client.astream() everywhere |
Routing Strategies
Pass strategy= to PlaudClient() to change routing behaviour. All 19 strategies share the same chat() / stream() / achat() / astream() API.
| Strategy | Default model | Notes |
|---|---|---|
FREE_FIRST (default) |
nvidia/nemotron-3-super-120b-a12b:free |
Dynamically picks any free model |
COST_OPTIMIZED |
dynamic | Sorts live catalog by price ascending |
QUALITY_FIRST |
anthropic/claude-opus-4.6-fast |
Highest context paid model |
ANTHROPIC_DIRECT |
claude-sonnet-4-6 |
Native Anthropic API (no OpenRouter) |
ANTHROPIC |
anthropic/claude-sonnet-4.6 |
via OpenRouter |
OPENAI |
openai/gpt-4o |
via OpenRouter (62 models) |
GOOGLE |
google/gemini-2.0-flash-001 |
via OpenRouter (32 models) |
META |
meta-llama/llama-4-maverick |
via OpenRouter (14 models) |
DEEPSEEK |
deepseek/deepseek-v3.2 |
via OpenRouter |
MISTRAL |
mistralai/mistral-small-2603 |
via OpenRouter |
XAI |
x-ai/grok-4.20 |
via OpenRouter |
PERPLEXITY |
perplexity/sonar-pro |
Search-grounded |
COHERE |
cohere/command-a |
via OpenRouter |
NOUS |
nousresearch/hermes-4-405b |
Hermes family |
QWEN |
qwen/qwen3.5-flash-02-23 |
Alibaba, via OpenRouter |
MINIMAX |
minimax/minimax-m1 |
via OpenRouter |
KIMI |
moonshotai/kimi-k2.5 |
Moonshot AI |
ZAI |
z-ai/glm-5.1 |
GLM family |
NVIDIA |
nvidia/nemotron-3-super-120b-a12b |
Nemotron family |
from plaudapi import PlaudClient
from plaudapi.router import RoutingStrategy
# Pin to a specific provider family
client = PlaudClient(strategy=RoutingStrategy.DEEPSEEK)
# Or override the model directly while keeping strategy routing
resp = client.chat(
messages=[{"role": "user", "content": "Hello"}],
model="deepseek/deepseek-r1",
)
Streaming
Sync streaming
from plaudapi import PlaudClient
client = PlaudClient(strategy="quality_first")
for chunk in client.stream([{"role": "user", "content": "Write me a haiku."}]):
print(chunk.delta, end="", flush=True)
if chunk.done:
print() # usage stats available on final chunk
Async streaming
import asyncio
from plaudapi import PlaudClient
async def main():
client = PlaudClient(strategy="openai")
async for chunk in client.astream(
[{"role": "user", "content": "Count to 5, one word per line."}]
):
print(chunk.delta, end="", flush=True)
asyncio.run(main())
Browse the Model Catalog
The catalog is fetched from OpenRouter with a 1-hour TTL cache at ~/.plaudapi/models_cache.json. No key required for reads.
List all models
models = client.list_models()
print(f"{len(models)} models available")
# Async
models = await client.alist_models()
Filter with find_models()
# All free models with at least 128k context
free_large = client.find_models(free=True, min_context=128_000)
# Anthropic models sorted cheapest-first
cheap_anthropic = client.find_models(
provider="anthropic",
free=False,
sort_by="price_asc",
)
# Vision-capable models
vision = client.find_models(modality="image", sort_by="context_desc")
for m in cheap_anthropic[:3]:
print(m["id"], "-", m["pricing"]["prompt"], "$/token")
find_models() supports: free, provider, min_context, modality, sort_by (price_asc, price_desc, context_desc).
Self-Healing
plaudapi bakes six self-healing behaviours directly into the client. You don't configure them — they just work.
1. Auth pre-check
Before the first network call, plaudapi verifies that the required API key is present. Missing keys raise PlaudAuthError immediately with the exact env var name and the fix:
PlaudAuthError: OPENROUTER_API_KEY is not set or invalid.
Run 'plaudapi setup' or add it to ~/.plaudapi/.env
2. Routing fallback
If the live OpenRouter catalog is unreachable (network error, rate limit, timeout), the router silently falls back to a hardcoded default model for the chosen strategy. Your code never sees the exception.
3. JSON auto-repair
When a model returns malformed JSON — trailing commas, unclosed braces, unterminated strings — repair_json() applies heuristic patches before surfacing the result:
from plaudapi.repair import repair_json
parsed, was_repaired = repair_json('{"key": "value",}')
# parsed = {"key": "value"}, was_repaired = True
4. Retry with full-jitter backoff
Network errors and 429 rate limits are retried up to 3 times using full-jitter exponential backoff (random delay in [0, min(cap, base * 2^attempt)]). Works identically for sync and async code.
5. strict_free guardrail
Set strict_free=True to guarantee your app never accidentally uses a paid model. If no free models are found in the catalog, plaudapi raises PlaudFallbackExhaustedError rather than silently upgrading to a paid tier.
client = PlaudClient(strict_free=True) # raises if zero free models available
6. Context window pre-check
PlaudRequest.estimated_tokens() counts your message length before sending. If it would exceed the model's context limit, PlaudContextWindowError is raised with the token count and the limit — so you can trim the message rather than burn the round-trip.
PlaudContextWindowError: Request has 142000 tokens but model limit is 128000.
Reduce your message length or choose a model with a larger context window.
Debug Mode
Set debug=True to print per-call telemetry to stdout:
client = PlaudClient(debug=True)
resp = client.chat([{"role": "user", "content": "Hi"}])
# [plaudapi] openai/gpt-4o · 12 in / 38 out · $0.00043 · 623ms · openrouter
Exception Reference
PlaudError # base
├── PlaudNetworkError # timeouts, connection failures
│ └── PlaudRateLimitError # HTTP 429, retries exhausted
├── PlaudAuthError # missing/invalid API key
├── PlaudFallbackExhaustedError # strict_free=True, no free models
└── PlaudLogicError # bad request shape
├── PlaudContextWindowError # exceeds model context limit
├── PlaudModelNotFoundError # model ID not in catalog (with suggestions)
└── PlaudContentPolicyError # HTTP 403, provider safety filter
Catch at whatever level makes sense for your application:
from plaudapi.exceptions import PlaudError, PlaudRateLimitError
try:
resp = client.chat(messages)
except PlaudRateLimitError:
# all retries failed — back off at the application level
...
except PlaudError as e:
# catch-all for any plaudapi error
...
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file plaudapi-0.1.0.tar.gz.
File metadata
- Download URL: plaudapi-0.1.0.tar.gz
- Upload date:
- Size: 18.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4517e18e684dea4d48cb9cd4584686ce089bed3085ae36feff67e01a7fb8ba72
|
|
| MD5 |
0d0b2c4c84e6d3f0704afc9e78a477d5
|
|
| BLAKE2b-256 |
7217a61cc953c961e914dc1e3fa935d9e4cdaa1f026b0ae27ebbfc836de28b01
|
File details
Details for the file plaudapi-0.1.0-py3-none-any.whl.
File metadata
- Download URL: plaudapi-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48780bfa0b5907cca6c289072e10cee9af079c8f0ebc124731bda0c0b0538b71
|
|
| MD5 |
984feb036a9ee530a52e9caa700ebe17
|
|
| BLAKE2b-256 |
c3081d1ca32d3d42628ac0afafc3ae6e38cb836b51356500c2423e83d620e75b
|