Skip to main content

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 automatic Idempotency-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, code and request_id;
  • retryable;
  • party: one of request, account, provider_key, provider, router or network.
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:
    • AuthenticationError and BadRequestError;
    • ProviderKeyError;
    • AllowanceExhaustedError and BudgetExceededError;
    • ProviderQuotaError.
  • Retried automatically:
    • RateLimitError (honours Retry-After);
    • ModelUnavailableError and ServerError;
    • ConnectionError and TimeoutError.
  • 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_limited
  • allowance_exhausted
  • insufficient_quota
  • provider_key_rejected
  • model_unavailable
  • server_error
  • fallback: 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)

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)

Source distribution for aistupidlevel 0.1.0
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)

Table of built distributions (wheels) for aistupidlevel 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page