SDKs

Onlist provides official SDKs for Python and JavaScript/TypeScript. They wrap the OpenAI client, so every chat.completions, embeddings, and images call works out of the box with zero configuration: the base URL and auth header are handled for you.

You do not need an SDK to use Onlist. Any OpenAI-compatible client works with a base URL swap (see Quickstart and Tool Configuration). The SDKs add convenience: auto-configured endpoints, ONLIST_API_KEY env var support, a typed marketplace namespace for browsing models and providers, and a typed account namespace for balance, API keys, and usage.

Python

Install

pip install onlist

Usage

from onlist import Onlist

client = Onlist()  # reads ONLIST_API_KEY env var

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

Authentication

The client looks for a key in this order:

  1. api_key constructor parameter
  2. ONLIST_API_KEY environment variable
  3. OPENAI_API_KEY environment variable (fallback for easy migration)
# Explicit key
client = Onlist(api_key="sk-...")

# Or set the env var
# export ONLIST_API_KEY=sk-...
client = Onlist()

Provider routing

Use extra_body to pass the provider object:

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

Streaming

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

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

While a reasoning model is still thinking, the stream may open with SSE comment heartbeats (: ONLIST PROCESSING). All official SDKs — Python, JavaScript, and the Vercel AI SDK provider — skip comment lines automatically, so your chunk loop only ever sees real chunks. If every upstream attempt fails after heartbeats began, the error arrives as an in-stream error frame, which the SDKs raise as an API error. See Streaming.

JavaScript / TypeScript

Install

npm install @onlist/sdk

Usage

import { Onlist } from "@onlist/sdk";

const client = new Onlist();  // reads ONLIST_API_KEY env var

const response = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-4",
  messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);

Authentication

The client looks for a key in this order:

  1. apiKey constructor parameter
  2. ONLIST_API_KEY environment variable
  3. OPENAI_API_KEY environment variable (fallback for easy migration)
// Explicit key
const client = new Onlist({ apiKey: "sk-..." });

// Or set the env var
// export ONLIST_API_KEY=sk-...
const client = new Onlist();

Provider routing

Pass the provider field directly:

const response = await client.chat.completions.create({
  model: "openai/gpt-4o",
  messages: [{ role: "user", content: "Hello" }],
  provider: { sort: "price" },
});

Streaming

const stream = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-4",
  messages: [{ role: "user", content: "Tell me a story" }],
  stream: true,
});

for await (const chunk of stream) {
  const content = chunk.choices[0]?.delta?.content;
  if (content) process.stdout.write(content);
}

Marketplace API

Both SDKs expose a marketplace namespace for browsing the Onlist catalog programmatically.

# List models with pricing
models = client.marketplace.models.list(limit=10)
for m in models.data:
    print(f"{m.id}: ${m.pricing.prompt}/M input tokens")

# Get a specific model with all provider offers
detail = client.marketplace.models.get("anthropic/claude-sonnet-4")
for offer in detail.providers:
    print(f"{offer.name}: ${offer.price_input_usd}/M input")

# List providers
providers = client.marketplace.providers.list()
for p in providers.items:
    print(f"{p.name} ({p.listing_count} models)")

Rankings API

The same namespace exposes the usage leaderboards behind onlist.io/rankings.

# Model usage leaderboard: sort="popular" | "trending", window="day" | "week" | "month"
rankings = client.marketplace.rankings.models(sort="popular", window="week")
for entry in rankings.leaderboard:
    print(f"#{entry.rank} {entry.model_name} — {entry.total_tokens} tokens")

# App usage rankings, optionally filtered by category
apps = client.marketplace.rankings.apps(sort="trending", window="month", category="coding")
for app in apps.apps:
    print(f"#{app.rank} {app.title} ({app.domain})")

Account API

Both SDKs wrap the Management API: balance, API key management, per-call costs, and daily usage.

Two credentials

Account endpoints authenticate with a management key (mgmt_...), which is a different credential from your inference key (sk-...) and cannot make model calls. Create one at Management keys.

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

If you pass only the API key, it is used for the account endpoints too. The SDK never inspects key prefixes locally, so the server decides: where a management key is actually required you get a PermissionDeniedError with the server's own message.

Five namespaces

They follow the wire paths one-for-one:

NamespaceEndpointWhat it does
credits/api/v1/creditsAccount balance
api_keys (apiKeys)/api/v1/key, /api/v1/keysCreate, list, update, delete inference keys
generations/api/v1/generationCost and timing for one call
activity/api/v1/activityDaily usage by model and provider
oauth/api/v1/auth/keysSign in with Onlist (PKCE exchange)
credits = client.credits.get()
print(f"${credits.total_credits - credits.total_usage:.4f} remaining")

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

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

generations.get() also accepts a plain inference key, which can look up the calls it made itself. Pass the X-Oneapi-Request-Id response header value.

Updating a key: three states

update() distinguishes "leave this alone" from "clear this". An omitted argument is not sent at all; an explicit None / null is sent as JSON null and clears the value.

client.api_keys.update("42", limit=None)        # remove the spend cap
client.api_keys.update("42", disabled=True)     # stop it spending
client.api_keys.update("42", name="renamed")    # limit untouched

In Python the "omitted" state is openai's own NOT_GIVEN sentinel, so it behaves exactly like optional arguments elsewhere in the openai client. In TypeScript, undefined means omitted and null means clear.

name and disabled deliberately do not accept null: 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. The type signatures prevent it.

Sign in with Onlist

Let your users authorize your app and receive their own inference key without pasting one. Both SDKs ship a PKCE helper.

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)

The exchange is a standalone function rather than a client method because an app running this flow has no API key yet — that is the whole point of it — and constructing a client requires one. 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.

See Management API for the full field reference, limits, and the raw HTTP shape.

Migration from OpenAI

Replace the import and constructor. All existing method calls stay the same.

- from openai import OpenAI
+ from onlist import Onlist

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

Migration from OpenRouter

Drop the baseURL override entirely.

- from openai import OpenAI
+ from onlist import Onlist

- client = OpenAI(
-     base_url="https://openrouter.ai/api/v1",
-     api_key=os.environ["OPENROUTER_API_KEY"],
- )
+ client = Onlist()