aistupidlevel
Python client for the AI Stupid Level Smart Router: one OpenAI-compatible endpoint that sends every request to the model measuring best for that kind of work, based on benchmarks that are re-run continuously. You call it with your own provider keys (OpenAI, Anthropic, Google, DeepSeek, Kimi, GLM).
- Sync and asyncio clients. Python 3.9+, and the only dependency is
httpx. - Routing on every answer: which model answered, why it was chosen, and whether a fallback answered.
- Safe retries: backoff,
Retry-After, and an automaticIdempotency-Key, so a retried request is never charged twice. - Typed errors that say whose problem it is: your request, your account, your provider key, the provider, or the network.
- Streaming, a tool-calling loop helper, route previews, usage, and a free sandbox for tests.
pip install aistupidlevel
Quick start
Create a router key at https://aistupidlevel.info/router/keys, add at least one provider key, and:
from aistupidlevel import ASLRouter
router = ASLRouter() # reads ASL_ROUTER_KEY
r = router.chat.completions.create(
model="auto-coding",
messages=[{"role": "user", "content": "Fix the off-by-one in this loop: ..."}],
)
print(r.choices[0].message.content)
print(r.routing.model) # e.g. "claude-opus-5-5"
print(r.routing.reasoning) # why it was chosen
Request parameters and response fields follow OpenAI Chat Completions: tools, tool_choice,
response_format, reasoning_effort, max_completion_tokens, and so on.
Responses are plain dicts that also allow attribute access, so r["choices"][0] and
r.choices[0] both work, and json.dumps(r) gives exactly the OpenAI shape.
Routing modes
Pass one of these as model, or any real model id (for example "gpt-6-sol") to pin a model:
auto: the strategy saved in your Preferences.auto-coding,auto-reasoning,auto-tooling: the top model on that benchmark.auto-best: the highest combined score.auto-value,auto-value-coding,auto-value-reasoning,auto-value-tooling: the most points per measured dollar, among models within 5 points of the best.auto-consistent,auto-fastest-quality: the steadiest, or the fastest, near-top model.auto-task: reads each request and picks the matching benchmark.auto-fastest,auto-cheapest: lowest latency, or lowest list price (no quality bar).
aistupidlevel.ROUTING_MODES lists them all.
Streaming
with router.chat.completions.create(model="auto-best", messages=messages, stream=True) as stream:
print(stream.routing.model) # known before the first chunk
for text in stream.text_stream():
print(text, end="", flush=True)
# or iterate chunks: for chunk in stream: ...
# or get the full answer: completion = stream.final_completion() # tool calls assembled for you
If a stream breaks after it has started, StreamInterruptedError is raised; it never ends silently.
Async
import asyncio
from aistupidlevel import AsyncASLRouter
async def main():
async with AsyncASLRouter() as router:
r = await router.chat.completions.create(model="auto-reasoning", messages=messages)
stream = await router.chat.completions.create(model="auto-best", messages=messages, stream=True)
async for text in stream.text_stream():
print(text, end="")
asyncio.run(main())
Tool calling
run_tools runs a tool-calling conversation through to the final answer:
- Each handler receives the tool's arguments as keyword arguments, and may be a plain function or,
with
AsyncASLRouter, a coroutine. - If a handler raises, the error goes to the model as the tool's result instead of crashing your app.
- The whole loop stays on one model.
def get_weather(city: str):
return {"city": city, "temp": 7}
out = router.chat.run_tools(
model="auto-tooling",
messages=[{"role": "user", "content": "Weather in Oslo?"}],
tools=[{"type": "function", "function": {"name": "get_weather", "parameters": {
"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}],
handlers={"get_weather": get_weather},
max_rounds=10,
)
print(out["completion"].choices[0].message.content, out["rounds"])
Errors
Every error is an ASLError with:
status,codeandrequest_id;retryable;party: one ofrequest,account,provider_key,provider,routerornetwork.
from aistupidlevel import ASLError, AllowanceExhaustedError, ProviderKeyError
try:
router.chat.completions.create(model="auto", messages=messages)
except AllowanceExhaustedError:
... # plan used up: upgrade or buy credits
except ProviderKeyError:
... # add or fix a provider key
except ASLError as e:
if e.retryable:
... # transient, and already retried twice
raise
The classes are:
- Not retried:
AuthenticationErrorandBadRequestError;ProviderKeyError;AllowanceExhaustedErrorandBudgetExceededError;ProviderQuotaError.
- Retried automatically:
RateLimitError(honoursRetry-After);ModelUnavailableErrorandServerError;ConnectionErrorandTimeoutError.
- Mid-stream:
StreamInterruptedError.
Retries use exponential backoff with jitter and respect the server's x-should-retry header.
Sandbox: test without spending anything
In sandbox mode each request goes through the real routing, as if you had a key for every provider.
It then gets a deterministic mock answer in exactly the real shape: streaming chunks, tool calls
with arguments that fit your schema, and JSON that fits your response_format. No provider is
called, and nothing is spent or counted.
test = ASLRouter(sandbox=True)
r = test.chat.completions.create(model="auto-coding", messages=messages)
assert r.routing.sandbox
# Make a request fail the way a real one can:
test.chat.completions.create(model="auto", messages=messages, simulate="allowance_exhausted")
# raises AllowanceExhaustedError
simulate accepts:
rate_limitedallowance_exhaustedinsufficient_quotaprovider_key_rejectedmodel_unavailableserver_errorfallback: the first model fails and the next one answers.
Preview, usage, feedback
p = router.route.preview(model="auto-coding", messages=messages) # nothing is sent or counted
p.primary.model, [f.model for f in p.fallbacks]
u = router.usage() # this month's requests, credits, and this key's budget
u.requests.remaining
router.feedback(r.id, "up") # ratings nudge future routing
Options
ASLRouter(
api_key="aism_...", # default: ASL_ROUTER_KEY
base_url="...", # default: https://aistupidlevel.info/v1 (or ASL_BASE_URL)
timeout=600.0, # per attempt, seconds. Reasoning models can think for minutes
max_retries=2,
sandbox=False,
auto_idempotency=True, # an Idempotency-Key on every non-streaming request
default_headers={},
http_client=None, # your own httpx.Client (proxies, transports, ...)
)
# Per request:
router.chat.completions.create(..., timeout=30, max_retries=0, extra_headers={},
idempotency_key="...", sandbox=True, simulate="fallback")
# A copy with some options changed:
fast = router.with_options(timeout=30, max_retries=0)
Links
MIT licence.
Metadata
Release files for aistupidlevel 0.1.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 |
|---|---|---|---|---|
| aistupidlevel-0.1.0.tar.gz | 18.6 kB | Without provenance | Details for aistupidlevel-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| aistupidlevel-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for aistupidlevel-0.1.0-py3-none-any.whl |
Total release size: 36.4 kB
Release files / aistupidlevel-0.1.0.tar.gz
| Download URL | aistupidlevel-0.1.0.tar.gz |
|---|---|
| Size | 18.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9aad5d65d78d6f83f01e592e0473202f020bf4e5157dc2650c5a17a8e4d4cf8c
|
|
BLAKE2b-256 checksum How to use checksums |
c72e0a81918e69847ac7ecb58deff23107b648dc1baef3fcdb4a74f195ac1bb1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / aistupidlevel-0.1.0-py3-none-any.whl
| Download URL | aistupidlevel-0.1.0-py3-none-any.whl |
|---|---|
| Size | 17.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7ee46a4751ba9a177186f518e6b4734c2267cb29102920d1918049e4111e6952
|
|
BLAKE2b-256 checksum How to use checksums |
e1e1e002c576d81091bf9c0109de9dadfe323910014225ee199192a53407ff16
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Security for aistupidlevel 0.1.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.