Skip to main content

Onlist Python SDK

The official Python client for Onlist, the AI API marketplace.

Onlist aggregates 40+ AI model providers behind a single OpenAI-compatible API. This SDK is a drop-in replacement for the OpenAI Python client, so you can switch with one line of code.

PyPI version Python versions License: MIT

Installation

pip install onlist

Quick Start

from onlist import Onlist

client = Onlist(api_key="sk-...")  # or set ONLIST_API_KEY env var

response = client.chat.completions.create(
    model="anthropic/claude-sonnet-4",
    messages=[{"role": "user", "content": "What is Onlist?"}],
)
print(response.choices[0].message.content)

Get your API key at onlist.io.

Authentication

The client reads your API key from:

  1. The api_key parameter
  2. The ONLIST_API_KEY environment variable
  3. The OPENAI_API_KEY environment variable (fallback, for easy migration)
export ONLIST_API_KEY="sk-..."

For the Account API there is a second, optional credential — a management key, read from the management_key parameter or ONLIST_MANAGEMENT_KEY, falling back to your API key:

export ONLIST_MANAGEMENT_KEY="mgmt_..."

Context Manager

Both sync and async clients support use as context managers, which ensures the underlying HTTP connections are properly closed:

from onlist import Onlist

with Onlist(api_key="sk-...") as client:
    response = client.chat.completions.create(
        model="anthropic/claude-sonnet-4",
        messages=[{"role": "user", "content": "Hello!"}],
    )
    print(response.choices[0].message.content)
# Connections are automatically closed here
import asyncio
from onlist import AsyncOnlist

async def main():
    async with AsyncOnlist(api_key="sk-...") as client:
        response = await client.chat.completions.create(
            model="openai/gpt-4o",
            messages=[{"role": "user", "content": "Hello!"}],
        )
        print(response.choices[0].message.content)

asyncio.run(main())

Provider Routing

Onlist's marketplace lets you choose which provider serves your request. Use the provider field via extra_body:

# Pin to a specific provider
response = client.chat.completions.create(
    model="anthropic/claude-sonnet-4",
    messages=[{"role": "user", "content": "Hello"}],
    extra_body={"provider": "alice-shop"},
)

# Route to the cheapest provider
response = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
    extra_body={"provider": {"sort": "price"}},
)

# Full routing control
response = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
    extra_body={
        "provider": {
            "allow": ["alice-shop", "bob-relay"],
            "sort": "price",
            "allow_fallbacks": True,
            "max_price": {"prompt": 0.000003, "completion": 0.000015},
        }
    },
)

You can also use the typed helper:

from onlist import ProviderRouting

routing = ProviderRouting(
    allow=["alice-shop", "bob-relay"],
    sort="price",
)

response = client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
    extra_body={"provider": routing.model_dump(exclude_none=True)},
)

Streaming

stream = client.chat.completions.create(
    model="anthropic/claude-sonnet-4",
    messages=[{"role": "user", "content": "Write a haiku about APIs"}],
    stream=True,
)

for chunk in stream:
    content = chunk.choices[0].delta.content
    if content:
        print(content, end="", flush=True)

Async Usage

import asyncio
from onlist import AsyncOnlist

async def main():
    client = AsyncOnlist(api_key="sk-...")

    response = await client.chat.completions.create(
        model="openai/gpt-4o",
        messages=[{"role": "user", "content": "Hello!"}],
    )
    print(response.choices[0].message.content)

asyncio.run(main())

Marketplace API

Query the Onlist marketplace for models and providers:

from onlist import Onlist

client = Onlist(api_key="sk-...")

# List available models with pricing
models = client.marketplace.models.list(limit=10)
for m in models.data:
    print(f"{m.id} - input: {m.pricing.prompt if m.pricing else 'N/A'}")

# Get detailed model info with all provider offers
detail = client.marketplace.models.get("anthropic/claude-sonnet-4")
print(f"{detail.id} - {len(detail.providers)} providers")

# Browse providers
providers = client.marketplace.providers.list()
for p in providers.data:
    print(f"{p.slug} - score: {p.score}")

# Get a specific provider's profile
provider = client.marketplace.providers.get("alice-shop")
print(f"{provider.display_name} - {provider.model_count} models")

Rankings API

View model usage rankings and app usage data:

from onlist import Onlist

client = Onlist(api_key="sk-...")

# Model usage leaderboard
rankings = client.marketplace.rankings.models(sort="popular", window="week")
for entry in rankings.leaderboard:
    print(f"#{entry.rank} {entry.model_name} by {entry.author} - {entry.total_tokens} tokens")

# Trending models
trending = client.marketplace.rankings.models(sort="trending", window="month")
for entry in trending.leaderboard:
    if entry.growth_pct is not None:
        print(f"{entry.model_name}: +{entry.growth_pct:.1f}%")

# App usage rankings
apps = client.marketplace.rankings.apps(sort="popular", window="month")
for app in apps.apps:
    print(f"#{app.rank} {app.title} ({app.domain}) - {app.total_requests} requests")

# Filter by category
coding_apps = client.marketplace.rankings.apps(category="coding")
for app in coding_apps.apps:
    print(f"{app.title}: {app.categories}")

Account API

Balance, API key management, per-call costs and daily usage. These endpoints are OpenRouter-compatible: same paths, same field names.

Most of them need a management key (mgmt_...), which you create at onlist.io/management-keys. It is a separate credential from your inference key and cannot make model calls:

from onlist import Onlist

client = Onlist(api_key="sk-...", management_key="mgmt_...")
# or set ONLIST_API_KEY and ONLIST_MANAGEMENT_KEY

If you pass only api_key, it is used for the account endpoints too. That matches OpenRouter's single-keyhole shape, and the server returns PermissionDeniedError where a management key is actually required.

Credits

credits = client.credits.get()
balance = credits.total_credits - credits.total_usage
print(f"${balance:.4f} remaining")

API keys

# Create — `.key` is the plaintext secret, returned exactly once
created = client.api_keys.create("ci-runner", limit=5.0, limit_reset="daily")
print(created.key)

# List — fixed pages of 100, no total; read until you get a short page
keys = client.api_keys.list(offset=0, include_disabled=True)

# Read one
key = client.api_keys.get(created.data.hash)

# Update — omitted arguments are left unchanged, `None` clears the value
client.api_keys.update(key.hash, limit=None)      # remove the spend cap
client.api_keys.update(key.hash, disabled=True)   # stop it spending

client.api_keys.delete(key.hash)

# Describe the credential this client is holding
me = client.api_keys.current()
print(me.label, me.is_management_key)

update() distinguishes three states. An omitted argument is left alone; an explicit None clears the value. Because of that, name and disabled do not accept None at all — the server reads {"name": null} as an empty name and {"disabled": null} as false, which would re-enable a key you only meant to leave alone.

limit_reset accepts "daily", "weekly", or None for a lifetime total.

Generation

What one call cost, and where it went. This one also accepts a plain inference key, which can look up the calls it made itself:

response = client.chat.completions.with_raw_response.create(
    model="deepseek/deepseek-chat",
    messages=[{"role": "user", "content": "Hello!"}],
)
gen = client.generations.get(response.headers["X-Oneapi-Request-Id"])
print(f"{gen.provider_name}: ${gen.total_cost:.6f}, {gen.latency}ms to first token")

Activity

Daily usage grouped by model and provider, covering the last 30 complete UTC days. Today is excluded, so the same query always returns the same numbers:

for row in client.activity.list():
    print(f"{row.date}  {row.model}  {row.requests} req  ${row.usage:.4f}")

# Narrow to one day or one key
client.activity.list(date="2026-09-05")
client.activity.list(api_key_hash="42")

Sign in with Onlist

Let your users authorize your app and get their own inference key, without ever pasting one. This is the OAuth PKCE flow, compatible with OpenRouter's:

import webbrowser
from onlist import Onlist, exchange_auth_code, generate_pkce

verifier, challenge = generate_pkce()

webbrowser.open(
    "https://onlist.io/auth"
    "?callback_url=http://localhost:8976/callback"
    f"&code_challenge={challenge}"
    "&code_challenge_method=S256"
)

# ...user approves, your callback receives ?code=...

result = exchange_auth_code(code, code_verifier=verifier)
client = Onlist(api_key=result.key)  # a new sk-... key, scoped to that user

exchange_auth_code() is a module-level function, not a client method, because an app running this flow has no API key yet — that is the whole point of it — and constructing Onlist requires one. async_exchange_auth_code() is the async version. If you already have a client, client.oauth.exchange() does the same thing.

The code is single-use and is consumed even when the verifier does not match, so a failed exchange means restarting the browser flow.

Other APIs

Since Onlist is fully OpenAI-compatible, all standard endpoints work:

# Embeddings
embedding = client.embeddings.create(
    model="openai/text-embedding-3-small",
    input="Hello world",
)

# Image generation
image = client.images.generate(
    model="openai/gpt-image-2",
    prompt="A sunset over Tokyo",
)

# Text-to-speech
audio = client.audio.speech.create(
    model="openai/tts-1",
    voice="alloy",
    input="Welcome to Onlist.",
)

Error Handling

For OpenAI-compatible API calls (chat.completions, embeddings, etc.), the standard openai exceptions are raised:

import openai
from onlist import Onlist

client = Onlist(api_key="sk-...")

try:
    response = client.chat.completions.create(
        model="openai/gpt-4o",
        messages=[{"role": "user", "content": "Hello"}],
    )
except openai.AuthenticationError:
    print("Invalid API key")
except openai.RateLimitError as e:
    print(f"Rate limited: {e.message}")

For marketplace API calls (client.marketplace.*), Onlist-specific exceptions are raised:

from onlist import Onlist, AuthenticationError, NotFoundError, APIError

client = Onlist(api_key="sk-...")

try:
    detail = client.marketplace.models.get("nonexistent/model")
except NotFoundError:
    print("Model not found")
except AuthenticationError:
    print("Invalid API key for marketplace")
except APIError as e:
    print(f"API error {e.status_code}: {e.message}")

Account API calls raise the same family, plus BadRequestError (400) and PermissionDeniedError (403). The server's message is passed through unchanged:

from onlist import Onlist, PermissionDeniedError

client = Onlist(api_key="sk-...")  # inference key only

try:
    client.credits.get()
except PermissionDeniedError as e:
    print(e.message)  # "Only management keys can perform this operation"

Marketplace requests are automatically retried on transient errors (408, 429, 5xx) with exponential backoff. Configure the retry limit:

# Disable retries
client = Onlist(api_key="sk-...", max_retries=0)

# More retries
client = Onlist(api_key="sk-...", max_retries=5)

Only GET requests are retried. Creating a key or exchanging an authorization code is never replayed: a duplicate key or a burnt code is worse than surfacing the transient error.

Migrate from OpenAI or OpenRouter

Already using the OpenAI SDK? Change one line:

- from openai import OpenAI
- client = OpenAI(api_key="sk-...")
+ from onlist import Onlist
+ client = Onlist(api_key="sk-...")

Or, if you prefer to keep using openai directly:

from openai import OpenAI

client = OpenAI(
    api_key="your-onlist-key",
    base_url="https://onlist.io/v1",
)

Routing Metadata

Onlist returns routing information in response headers. Access them to see which provider actually served your request:

# Use the with_raw_response pattern from the openai SDK:
raw_response = client.chat.completions.with_raw_response.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
)
print(raw_response.headers.get("x-onlist-route-id"))
print(raw_response.headers.get("x-onlist-provider"))

# Parse the completion as usual:
response = raw_response.parse()
print(response.choices[0].message.content)

Links

Release files for onlist 0.3.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 onlist 0.3.0
File Size Uploaded
onlist-0.3.0.tar.gz 31.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for onlist 0.3.0
File Interpreter ABI Platform
onlist-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size:57.9 kB

Release files / onlist-0.3.0.tar.gz

Download URL onlist-0.3.0.tar.gz
Size 31.6 kB
Tags Source
SHA-256 checksum
How to use checksums
9ce15be084526f86b3705a6420441f943db075a0f9f6d138ff72736216ea76e5
BLAKE2b-256 checksum
How to use checksums
0b50c4542c52fc7a1060b6e54f2af141cf483973a6d7b882ee26003ba8311e07
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 Sep 7, 2026.

Transparency log

Release files / onlist-0.3.0-py3-none-any.whl

Download URL onlist-0.3.0-py3-none-any.whl
Size 26.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8a60d2ed75b07d9095f3e506d1d490da131b1b396964b8a90a543487d944cdad
BLAKE2b-256 checksum
How to use checksums
b05ad42daa0ad3059e779bf833f6de7c00e64127f2393ded2c386307cb811174
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 Sep 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

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