onlist 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
onlist/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """Onlist Python SDK -- Official client for the Onlist AI API marketplace."""
2
+
3
+ from onlist._client import AsyncOnlist, Onlist
4
+ from onlist._exceptions import (
5
+ APIError,
6
+ AuthenticationError,
7
+ InsufficientBalanceError,
8
+ OnlistError,
9
+ ProviderError,
10
+ RateLimitError,
11
+ )
12
+ from onlist._version import __version__
13
+ from onlist.types import (
14
+ MaxPrice,
15
+ Model,
16
+ ModelDetail,
17
+ ModelListResponse,
18
+ Pricing,
19
+ Provider,
20
+ ProviderDetail,
21
+ ProviderListResponse,
22
+ ProviderRouting,
23
+ )
24
+
25
+ __all__ = [
26
+ # Clients
27
+ "AsyncOnlist",
28
+ "Onlist",
29
+ # Exceptions
30
+ "APIError",
31
+ "AuthenticationError",
32
+ "InsufficientBalanceError",
33
+ "OnlistError",
34
+ "ProviderError",
35
+ "RateLimitError",
36
+ # Types
37
+ "MaxPrice",
38
+ "Model",
39
+ "ModelDetail",
40
+ "ModelListResponse",
41
+ "Pricing",
42
+ "Provider",
43
+ "ProviderDetail",
44
+ "ProviderListResponse",
45
+ "ProviderRouting",
46
+ # Meta
47
+ "__version__",
48
+ ]
onlist/_client.py ADDED
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any, Mapping
5
+
6
+ import httpx
7
+ import openai
8
+
9
+ from onlist._constants import BASE_URL, ENV_API_KEY, MARKETPLACE_BASE_URL
10
+ from onlist._version import __version__
11
+ from onlist.resources.marketplace import AsyncMarketplace, Marketplace
12
+
13
+
14
+ class Onlist(openai.OpenAI):
15
+ """Onlist API client. Drop-in replacement for ``openai.OpenAI``.
16
+
17
+ All OpenAI-compatible methods (``chat.completions``, ``embeddings``,
18
+ ``images``, ``audio``, ``models``) work identically. The ``marketplace``
19
+ attribute provides access to Onlist-specific APIs.
20
+
21
+ Example::
22
+
23
+ from onlist import Onlist
24
+
25
+ client = Onlist(api_key="sk-...")
26
+
27
+ # OpenAI-compatible
28
+ response = client.chat.completions.create(
29
+ model="anthropic/claude-sonnet-4",
30
+ messages=[{"role": "user", "content": "Hello!"}],
31
+ )
32
+
33
+ # Onlist marketplace
34
+ models = client.marketplace.models.list()
35
+ """
36
+
37
+ marketplace: Marketplace
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ api_key: str | None = None,
43
+ base_url: str | httpx.URL | None = None,
44
+ default_headers: Mapping[str, str] | None = None,
45
+ **kwargs: Any,
46
+ ) -> None:
47
+ resolved_key = api_key or os.environ.get(ENV_API_KEY)
48
+
49
+ merged_headers = dict(default_headers or {})
50
+ merged_headers.setdefault("User-Agent", f"onlist-python/{__version__}")
51
+ merged_headers.setdefault("HTTP-Referer", "https://onlist.io")
52
+
53
+ super().__init__(
54
+ api_key=resolved_key,
55
+ base_url=base_url or BASE_URL,
56
+ default_headers=merged_headers,
57
+ **kwargs,
58
+ )
59
+
60
+ marketplace_base = str(self.base_url).split("/v1")[0]
61
+
62
+ effective_key = self.api_key if isinstance(self.api_key, str) else resolved_key
63
+ self.marketplace = Marketplace(
64
+ api_key=effective_key,
65
+ base_url=marketplace_base or MARKETPLACE_BASE_URL,
66
+ )
67
+
68
+ def close(self) -> None:
69
+ super().close()
70
+ self.marketplace.close()
71
+
72
+
73
+ class AsyncOnlist(openai.AsyncOpenAI):
74
+ """Async Onlist API client. Drop-in replacement for ``openai.AsyncOpenAI``.
75
+
76
+ Example::
77
+
78
+ import asyncio
79
+ from onlist import AsyncOnlist
80
+
81
+ async def main():
82
+ client = AsyncOnlist(api_key="sk-...")
83
+ response = await client.chat.completions.create(
84
+ model="openai/gpt-4o",
85
+ messages=[{"role": "user", "content": "Hello!"}],
86
+ )
87
+ print(response.choices[0].message.content)
88
+
89
+ asyncio.run(main())
90
+ """
91
+
92
+ marketplace: AsyncMarketplace
93
+
94
+ def __init__(
95
+ self,
96
+ *,
97
+ api_key: str | None = None,
98
+ base_url: str | httpx.URL | None = None,
99
+ default_headers: Mapping[str, str] | None = None,
100
+ **kwargs: Any,
101
+ ) -> None:
102
+ resolved_key = api_key or os.environ.get(ENV_API_KEY)
103
+
104
+ merged_headers = dict(default_headers or {})
105
+ merged_headers.setdefault("User-Agent", f"onlist-python/{__version__}")
106
+ merged_headers.setdefault("HTTP-Referer", "https://onlist.io")
107
+
108
+ super().__init__(
109
+ api_key=resolved_key,
110
+ base_url=base_url or BASE_URL,
111
+ default_headers=merged_headers,
112
+ **kwargs,
113
+ )
114
+
115
+ marketplace_base = str(self.base_url).split("/v1")[0]
116
+
117
+ effective_key = self.api_key if isinstance(self.api_key, str) else resolved_key
118
+ self.marketplace = AsyncMarketplace(
119
+ api_key=effective_key,
120
+ base_url=marketplace_base or MARKETPLACE_BASE_URL,
121
+ )
122
+
123
+ async def close(self) -> None:
124
+ await super().close()
125
+ await self.marketplace.close()
onlist/_constants.py ADDED
@@ -0,0 +1,3 @@
1
+ BASE_URL = "https://onlist.io/v1"
2
+ MARKETPLACE_BASE_URL = "https://onlist.io"
3
+ ENV_API_KEY = "ONLIST_API_KEY"
onlist/_exceptions.py ADDED
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class OnlistError(Exception):
7
+ """Base exception for all Onlist SDK errors."""
8
+
9
+ def __init__(self, message: str) -> None:
10
+ super().__init__(message)
11
+ self.message = message
12
+
13
+
14
+ class APIError(OnlistError):
15
+ """An error returned by the Onlist API."""
16
+
17
+ status_code: int
18
+ type: str | None
19
+ code: str | None
20
+ param: str | None
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ status_code: int,
27
+ type: str | None = None,
28
+ code: str | None = None,
29
+ param: str | None = None,
30
+ body: Any = None,
31
+ ) -> None:
32
+ super().__init__(message)
33
+ self.status_code = status_code
34
+ self.type = type
35
+ self.code = code
36
+ self.param = param
37
+ self.body = body
38
+
39
+ def __repr__(self) -> str:
40
+ return (
41
+ f"{self.__class__.__name__}("
42
+ f"message={self.message!r}, "
43
+ f"status_code={self.status_code}, "
44
+ f"code={self.code!r})"
45
+ )
46
+
47
+
48
+ class AuthenticationError(APIError):
49
+ """Raised on 401 responses (missing or invalid API key)."""
50
+
51
+ def __init__(self, message: str = "Invalid API key", **kwargs: Any) -> None:
52
+ kwargs.setdefault("status_code", 401)
53
+ super().__init__(message, **kwargs)
54
+
55
+
56
+ class InsufficientBalanceError(APIError):
57
+ """Raised on 402 responses (insufficient balance)."""
58
+
59
+ def __init__(self, message: str = "Insufficient balance", **kwargs: Any) -> None:
60
+ kwargs.setdefault("status_code", 402)
61
+ super().__init__(message, **kwargs)
62
+
63
+
64
+ class RateLimitError(APIError):
65
+ """Raised on 429 responses (rate limited)."""
66
+
67
+ def __init__(self, message: str = "Rate limited", **kwargs: Any) -> None:
68
+ kwargs.setdefault("status_code", 429)
69
+ super().__init__(message, **kwargs)
70
+
71
+
72
+ class ProviderError(APIError):
73
+ """Raised when no provider is available for the request."""
74
+
75
+ pass
76
+
77
+
78
+ def _raise_for_status(status_code: int, body: Any) -> None:
79
+ """Parse an Onlist error response and raise the appropriate exception."""
80
+ error = {}
81
+ if isinstance(body, dict):
82
+ error = body.get("error", body)
83
+
84
+ message = error.get("message", str(body)) if isinstance(error, dict) else str(body)
85
+ etype = error.get("type") if isinstance(error, dict) else None
86
+ code = error.get("code") if isinstance(error, dict) else None
87
+ param = error.get("param") if isinstance(error, dict) else None
88
+
89
+ kwargs = dict(
90
+ status_code=status_code,
91
+ type=etype,
92
+ code=code,
93
+ param=param,
94
+ body=body,
95
+ )
96
+
97
+ if status_code == 401:
98
+ raise AuthenticationError(message, **kwargs)
99
+ if status_code == 402:
100
+ raise InsufficientBalanceError(message, **kwargs)
101
+ if status_code == 429:
102
+ raise RateLimitError(message, **kwargs)
103
+ if code and code.startswith("no_provider"):
104
+ raise ProviderError(message, **kwargs)
105
+
106
+ raise APIError(message, **kwargs)
onlist/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
onlist/py.typed ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from onlist.resources.marketplace import AsyncMarketplace, Marketplace
2
+
3
+ __all__ = ["Marketplace", "AsyncMarketplace"]
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from urllib.parse import quote
5
+
6
+ import httpx
7
+
8
+ from onlist._exceptions import _raise_for_status
9
+ from onlist._version import __version__
10
+ from onlist.types.model import ModelDetail, ModelListResponse
11
+ from onlist.types.provider import Provider, ProviderDetail, ProviderListResponse
12
+
13
+
14
+ _DEFAULT_TIMEOUT = 30.0
15
+
16
+
17
+ def _default_headers(api_key: str | None) -> dict[str, str]:
18
+ headers: dict[str, str] = {
19
+ "User-Agent": f"onlist-python/{__version__}",
20
+ "Accept": "application/json",
21
+ }
22
+ if api_key:
23
+ headers["Authorization"] = f"Bearer {api_key}"
24
+ return headers
25
+
26
+
27
+ def _encode_path(segment: str) -> str:
28
+ """URL-encode a path segment, preserving ``/`` for model IDs like ``author/model``."""
29
+ return quote(segment, safe="/")
30
+
31
+
32
+ def _parse_response(response: httpx.Response) -> Any:
33
+ if response.status_code >= 400:
34
+ try:
35
+ body = response.json()
36
+ except Exception:
37
+ body = response.text
38
+ _raise_for_status(response.status_code, body)
39
+ body = response.json()
40
+ if isinstance(body, dict) and "data" in body and "success" in body:
41
+ body = body["data"]
42
+ return body
43
+
44
+
45
+ class MarketplaceModels:
46
+ """Query the Onlist model catalog."""
47
+
48
+ def __init__(self, client: httpx.Client) -> None:
49
+ self._client = client
50
+
51
+ def list(
52
+ self,
53
+ *,
54
+ limit: int = 20,
55
+ offset: int = 0,
56
+ q: str | None = None,
57
+ ) -> ModelListResponse:
58
+ """List models with pricing and provider counts.
59
+
60
+ Args:
61
+ limit: Maximum results to return.
62
+ offset: Number of results to skip.
63
+ q: Optional search query.
64
+ """
65
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
66
+ if q:
67
+ params["q"] = q
68
+ resp = self._client.get("/api/mkt/models", params=params)
69
+ return ModelListResponse.model_validate(_parse_response(resp))
70
+
71
+ def get(self, model_id: str) -> ModelDetail:
72
+ """Get detailed info for a model, including all provider offers.
73
+
74
+ Args:
75
+ model_id: Model identifier, e.g. ``"anthropic/claude-sonnet-4-6"``.
76
+ """
77
+ resp = self._client.get(f"/api/mkt/models/{_encode_path(model_id)}")
78
+ data = _parse_response(resp)
79
+ if isinstance(data, dict) and "data" in data:
80
+ data = data["data"]
81
+ return ModelDetail.model_validate(data)
82
+
83
+
84
+ class MarketplaceProviders:
85
+ """Query provider (seller) profiles."""
86
+
87
+ def __init__(self, client: httpx.Client) -> None:
88
+ self._client = client
89
+
90
+ def list(
91
+ self,
92
+ *,
93
+ sort: str | None = None,
94
+ q: str | None = None,
95
+ ) -> ProviderListResponse:
96
+ """List all providers on the marketplace.
97
+
98
+ Args:
99
+ sort: Sort order (e.g. ``"score"``).
100
+ q: Optional search query.
101
+ """
102
+ params: dict[str, Any] = {}
103
+ if sort:
104
+ params["sort"] = sort
105
+ if q:
106
+ params["q"] = q
107
+ resp = self._client.get("/api/mkt/providers", params=params)
108
+ return ProviderListResponse.model_validate(_parse_response(resp))
109
+
110
+ def get(self, slug: str) -> ProviderDetail:
111
+ """Get a provider's public profile by slug.
112
+
113
+ Args:
114
+ slug: Provider slug, e.g. ``"alice-shop"``.
115
+ """
116
+ resp = self._client.get(f"/api/mkt/provider/{_encode_path(slug)}")
117
+ data = _parse_response(resp)
118
+ if isinstance(data, dict) and "data" in data:
119
+ data = data["data"]
120
+ return ProviderDetail.model_validate(data)
121
+
122
+
123
+ class Marketplace:
124
+ """Access Onlist marketplace data.
125
+
126
+ Provides read-only access to the public marketplace APIs:
127
+ model catalog, provider directory, and more.
128
+ """
129
+
130
+ models: MarketplaceModels
131
+ providers: MarketplaceProviders
132
+
133
+ def __init__(
134
+ self,
135
+ *,
136
+ api_key: str | None = None,
137
+ base_url: str,
138
+ timeout: float = _DEFAULT_TIMEOUT,
139
+ ) -> None:
140
+ self._client = httpx.Client(
141
+ base_url=base_url,
142
+ headers=_default_headers(api_key),
143
+ timeout=timeout,
144
+ )
145
+ self.models = MarketplaceModels(self._client)
146
+ self.providers = MarketplaceProviders(self._client)
147
+
148
+ def close(self) -> None:
149
+ self._client.close()
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Async mirror
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ class AsyncMarketplaceModels:
158
+ def __init__(self, client: httpx.AsyncClient) -> None:
159
+ self._client = client
160
+
161
+ async def list(
162
+ self,
163
+ *,
164
+ limit: int = 20,
165
+ offset: int = 0,
166
+ q: str | None = None,
167
+ ) -> ModelListResponse:
168
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
169
+ if q:
170
+ params["q"] = q
171
+ resp = await self._client.get("/api/mkt/models", params=params)
172
+ return ModelListResponse.model_validate(_parse_response(resp))
173
+
174
+ async def get(self, model_id: str) -> ModelDetail:
175
+ resp = await self._client.get(f"/api/mkt/models/{_encode_path(model_id)}")
176
+ data = _parse_response(resp)
177
+ if isinstance(data, dict) and "data" in data:
178
+ data = data["data"]
179
+ return ModelDetail.model_validate(data)
180
+
181
+
182
+ class AsyncMarketplaceProviders:
183
+ def __init__(self, client: httpx.AsyncClient) -> None:
184
+ self._client = client
185
+
186
+ async def list(
187
+ self,
188
+ *,
189
+ sort: str | None = None,
190
+ q: str | None = None,
191
+ ) -> ProviderListResponse:
192
+ params: dict[str, Any] = {}
193
+ if sort:
194
+ params["sort"] = sort
195
+ if q:
196
+ params["q"] = q
197
+ resp = await self._client.get("/api/mkt/providers", params=params)
198
+ return ProviderListResponse.model_validate(_parse_response(resp))
199
+
200
+ async def get(self, slug: str) -> ProviderDetail:
201
+ resp = await self._client.get(f"/api/mkt/provider/{_encode_path(slug)}")
202
+ data = _parse_response(resp)
203
+ if isinstance(data, dict) and "data" in data:
204
+ data = data["data"]
205
+ return ProviderDetail.model_validate(data)
206
+
207
+
208
+ class AsyncMarketplace:
209
+ """Async version of :class:`Marketplace`."""
210
+
211
+ models: AsyncMarketplaceModels
212
+ providers: AsyncMarketplaceProviders
213
+
214
+ def __init__(
215
+ self,
216
+ *,
217
+ api_key: str | None = None,
218
+ base_url: str,
219
+ timeout: float = _DEFAULT_TIMEOUT,
220
+ ) -> None:
221
+ self._client = httpx.AsyncClient(
222
+ base_url=base_url,
223
+ headers=_default_headers(api_key),
224
+ timeout=timeout,
225
+ )
226
+ self.models = AsyncMarketplaceModels(self._client)
227
+ self.providers = AsyncMarketplaceProviders(self._client)
228
+
229
+ async def close(self) -> None:
230
+ await self._client.aclose()
@@ -0,0 +1,28 @@
1
+ from onlist.types.model import (
2
+ Architecture,
3
+ Model,
4
+ ModelDetail,
5
+ ModelListResponse,
6
+ Pricing,
7
+ TopProvider,
8
+ )
9
+ from onlist.types.provider import (
10
+ Provider,
11
+ ProviderDetail,
12
+ ProviderListResponse,
13
+ )
14
+ from onlist.types.routing import MaxPrice, ProviderRouting
15
+
16
+ __all__ = [
17
+ "Architecture",
18
+ "MaxPrice",
19
+ "Model",
20
+ "ModelDetail",
21
+ "ModelListResponse",
22
+ "Pricing",
23
+ "Provider",
24
+ "ProviderDetail",
25
+ "ProviderListResponse",
26
+ "ProviderRouting",
27
+ "TopProvider",
28
+ ]
onlist/types/model.py ADDED
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Pricing(BaseModel):
9
+ prompt: str = "0"
10
+ completion: str = "0"
11
+ request: str | None = None
12
+
13
+
14
+ class Architecture(BaseModel):
15
+ modality: str | None = None
16
+ input_modalities: list[str] | None = None
17
+ output_modalities: list[str] | None = None
18
+ tokenizer: str | None = None
19
+
20
+
21
+ class TopProvider(BaseModel):
22
+ context_length: int | None = None
23
+ max_completion_tokens: int | None = None
24
+ is_moderated: bool | None = None
25
+
26
+
27
+ class Model(BaseModel):
28
+ id: str
29
+ name: str | None = None
30
+ author: str | None = None
31
+ owned_by: str | None = None
32
+ canonical_slug: str | None = None
33
+ created: int | None = None
34
+ description: str | None = None
35
+ context_length: int | None = None
36
+ max_output_length: int | None = None
37
+ architecture: Architecture | None = None
38
+ pricing: Pricing | None = None
39
+ supported_parameters: list[str] | None = None
40
+ quantization: str | None = None
41
+ top_provider: TopProvider | None = None
42
+ is_ready: bool | None = None
43
+
44
+ model_config = {"extra": "allow"}
45
+
46
+
47
+ class ProviderOffer(BaseModel):
48
+ listing_id: int | None = None
49
+ provider_id: int | None = None
50
+ slug: str | None = None
51
+ name: str | None = None
52
+ logo_url: str | None = None
53
+ score: float | None = None
54
+ price_input_usd: str | None = None
55
+ price_output_usd: str | None = None
56
+ availability_7d: float | None = None
57
+
58
+ model_config = {"extra": "allow"}
59
+
60
+
61
+ class ModelDetail(BaseModel):
62
+ id: str
63
+ name: str | None = None
64
+ author: str | None = None
65
+ owned_by: str | None = None
66
+ context_length: int | None = None
67
+ max_output_length: int | None = None
68
+ architecture: Architecture | None = None
69
+ pricing: Pricing | None = None
70
+ description: str | None = None
71
+ providers: list[ProviderOffer] = Field(default_factory=list)
72
+
73
+ model_config = {"extra": "allow"}
74
+
75
+
76
+ class ModelListResponse(BaseModel):
77
+ """Response from ``marketplace.models.list()``."""
78
+
79
+ data: list[dict[str, Any]] = Field(default_factory=list)
80
+ total: int | None = None
81
+ offset: int | None = None
82
+ limit: int | None = None
83
+
84
+ model_config = {"extra": "allow"}
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Provider(BaseModel):
9
+ id: int | None = None
10
+ slug: str
11
+ name: str | None = None
12
+ display_name: str | None = None
13
+ description: str | None = None
14
+ logo_url: str | None = None
15
+ listing_count: int | None = None
16
+ follower_count: int | None = None
17
+ weighted_score: float | None = None
18
+ sample_count: int | None = None
19
+ max_rpm: int | None = None
20
+ availability_7d: float | None = None
21
+
22
+ @property
23
+ def score(self) -> float | None:
24
+ return self.weighted_score
25
+
26
+ @property
27
+ def model_count(self) -> int | None:
28
+ return self.listing_count
29
+
30
+ model_config = {"extra": "allow"}
31
+
32
+
33
+ class ProviderDetail(BaseModel):
34
+ id: int | None = None
35
+ slug: str
36
+ name: str | None = None
37
+ display_name: str | None = None
38
+ description: str | None = None
39
+ logo_url: str | None = None
40
+ listing_count: int | None = None
41
+ follower_count: int | None = None
42
+ weighted_score: float | None = None
43
+ max_rpm: int | None = None
44
+ availability_7d: float | None = None
45
+ listings: list[dict[str, Any]] = Field(default_factory=list)
46
+
47
+ @property
48
+ def score(self) -> float | None:
49
+ return self.weighted_score
50
+
51
+ @property
52
+ def model_count(self) -> int | None:
53
+ return self.listing_count
54
+
55
+ model_config = {"extra": "allow"}
56
+
57
+
58
+ class ProviderListResponse(BaseModel):
59
+ """Response from ``marketplace.providers.list()``."""
60
+
61
+ items: list[Provider] = Field(default_factory=list)
62
+
63
+ @property
64
+ def data(self) -> list[Provider]:
65
+ return self.items
66
+
67
+ @property
68
+ def total(self) -> int:
69
+ return len(self.items)
70
+
71
+ model_config = {"extra": "allow"}
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class MaxPrice(BaseModel):
9
+ """USD-per-token price caps for provider routing."""
10
+
11
+ prompt: float | None = None
12
+ completion: float | None = None
13
+
14
+
15
+ class ProviderRouting(BaseModel):
16
+ """OpenRouter-compatible provider routing controls.
17
+
18
+ Pass as ``extra_body={"provider": routing.model_dump(exclude_none=True)}``
19
+ or simply as a dict.
20
+
21
+ Example::
22
+
23
+ from onlist.types import ProviderRouting
24
+
25
+ routing = ProviderRouting(only=["alice-shop"], sort="price")
26
+ client.chat.completions.create(
27
+ model="anthropic/claude-sonnet-4",
28
+ messages=[...],
29
+ extra_body={"provider": routing.model_dump(exclude_none=True)},
30
+ )
31
+ """
32
+
33
+ only: list[str] | None = None
34
+ sort: Literal["price", "throughput"] | None = None
35
+ order: list[str] | None = None
36
+ allow: list[str] | None = None
37
+ ignore: list[str] | None = None
38
+ allow_fallbacks: bool | None = None
39
+ max_price: MaxPrice | None = None
@@ -0,0 +1,303 @@
1
+ Metadata-Version: 2.4
2
+ Name: onlist
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Onlist, the AI API marketplace. Access GPT, Claude, Gemini, DeepSeek and 40+ models through one OpenAI-compatible API.
5
+ Project-URL: Homepage, https://onlist.io
6
+ Project-URL: Documentation, https://onlist.io/docs
7
+ Project-URL: Repository, https://github.com/OnlistTeam/onlist-python
8
+ Project-URL: API Reference, https://onlist.io/docs/api
9
+ Project-URL: Model Catalog, https://onlist.io/models
10
+ Project-URL: Provider Directory, https://onlist.io/providers
11
+ Project-URL: Changelog, https://github.com/OnlistTeam/onlist-python/releases
12
+ Project-URL: Issues, https://github.com/OnlistTeam/onlist-python/issues
13
+ Author-email: Onlist <dev@onlist.io>
14
+ License-Expression: MIT
15
+ License-File: LICENSE
16
+ Keywords: ai,ai-api,ai-gateway,anthropic,api,chat-completion,claude,deepseek,gemini,gpt,llm,llm-proxy,marketplace,model-router,onlist,openai,openrouter,sdk
17
+ Classifier: Development Status :: 4 - Beta
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.9
30
+ Requires-Dist: httpx>=0.25.0
31
+ Requires-Dist: openai>=1.0.0
32
+ Requires-Dist: pydantic>=2.0.0
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Onlist Python SDK
36
+
37
+ The official Python client for [Onlist](https://onlist.io), the AI API marketplace.
38
+
39
+ Onlist aggregates 40+ AI model providers behind a single OpenAI-compatible API.
40
+ This SDK is a drop-in replacement for the OpenAI Python client, so you can switch
41
+ with one line of code.
42
+
43
+ [![PyPI version](https://img.shields.io/pypi/v/onlist.svg)](https://pypi.org/project/onlist/)
44
+ [![Python versions](https://img.shields.io/pypi/pyversions/onlist.svg)](https://pypi.org/project/onlist/)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/OnlistTeam/onlist-python/blob/main/LICENSE)
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install onlist
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ from onlist import Onlist
57
+
58
+ client = Onlist(api_key="sk-...") # or set ONLIST_API_KEY env var
59
+
60
+ response = client.chat.completions.create(
61
+ model="anthropic/claude-sonnet-4",
62
+ messages=[{"role": "user", "content": "What is Onlist?"}],
63
+ )
64
+ print(response.choices[0].message.content)
65
+ ```
66
+
67
+ Get your API key at [onlist.io](https://onlist.io).
68
+
69
+ ## Authentication
70
+
71
+ The client reads your API key from:
72
+ 1. The `api_key` parameter
73
+ 2. The `ONLIST_API_KEY` environment variable
74
+ 3. The `OPENAI_API_KEY` environment variable (fallback, for easy migration)
75
+
76
+ ```bash
77
+ export ONLIST_API_KEY="sk-..."
78
+ ```
79
+
80
+ ## Provider Routing
81
+
82
+ Onlist's marketplace lets you choose which provider serves your request.
83
+ Use the `provider` field via `extra_body`:
84
+
85
+ ```python
86
+ # Pin to a specific provider
87
+ response = client.chat.completions.create(
88
+ model="anthropic/claude-sonnet-4",
89
+ messages=[{"role": "user", "content": "Hello"}],
90
+ extra_body={"provider": "alice-shop"},
91
+ )
92
+
93
+ # Route to the cheapest provider
94
+ response = client.chat.completions.create(
95
+ model="openai/gpt-4o",
96
+ messages=[{"role": "user", "content": "Hello"}],
97
+ extra_body={"provider": {"sort": "price"}},
98
+ )
99
+
100
+ # Full routing control
101
+ response = client.chat.completions.create(
102
+ model="openai/gpt-4o",
103
+ messages=[{"role": "user", "content": "Hello"}],
104
+ extra_body={
105
+ "provider": {
106
+ "allow": ["alice-shop", "bob-relay"],
107
+ "sort": "price",
108
+ "allow_fallbacks": True,
109
+ "max_price": {"prompt": 0.000003, "completion": 0.000015},
110
+ }
111
+ },
112
+ )
113
+ ```
114
+
115
+ You can also use the typed helper:
116
+
117
+ ```python
118
+ from onlist import ProviderRouting
119
+
120
+ routing = ProviderRouting(
121
+ allow=["alice-shop", "bob-relay"],
122
+ sort="price",
123
+ )
124
+
125
+ response = client.chat.completions.create(
126
+ model="openai/gpt-4o",
127
+ messages=[{"role": "user", "content": "Hello"}],
128
+ extra_body={"provider": routing.model_dump(exclude_none=True)},
129
+ )
130
+ ```
131
+
132
+ ## Streaming
133
+
134
+ ```python
135
+ stream = client.chat.completions.create(
136
+ model="anthropic/claude-sonnet-4",
137
+ messages=[{"role": "user", "content": "Write a haiku about APIs"}],
138
+ stream=True,
139
+ )
140
+
141
+ for chunk in stream:
142
+ content = chunk.choices[0].delta.content
143
+ if content:
144
+ print(content, end="", flush=True)
145
+ ```
146
+
147
+ ## Async Usage
148
+
149
+ ```python
150
+ import asyncio
151
+ from onlist import AsyncOnlist
152
+
153
+ async def main():
154
+ client = AsyncOnlist(api_key="sk-...")
155
+
156
+ response = await client.chat.completions.create(
157
+ model="openai/gpt-4o",
158
+ messages=[{"role": "user", "content": "Hello!"}],
159
+ )
160
+ print(response.choices[0].message.content)
161
+
162
+ asyncio.run(main())
163
+ ```
164
+
165
+ ## Marketplace API
166
+
167
+ Query the Onlist marketplace for models and providers:
168
+
169
+ ```python
170
+ from onlist import Onlist
171
+
172
+ client = Onlist(api_key="sk-...")
173
+
174
+ # List available models with pricing
175
+ models = client.marketplace.models.list(limit=10)
176
+ for m in models.data:
177
+ print(f"{m['id']} - input: {m.get('pricing', {}).get('prompt', 'N/A')}")
178
+
179
+ # Get detailed model info with all provider offers
180
+ detail = client.marketplace.models.get("anthropic/claude-sonnet-4")
181
+ print(f"{detail.id} - {len(detail.providers)} providers")
182
+
183
+ # Browse providers
184
+ providers = client.marketplace.providers.list()
185
+ for p in providers.data:
186
+ print(f"{p.slug} - score: {p.score}")
187
+
188
+ # Get a specific provider's profile
189
+ provider = client.marketplace.providers.get("alice-shop")
190
+ print(f"{provider.display_name} - {provider.model_count} models")
191
+ ```
192
+
193
+ ## Other APIs
194
+
195
+ Since Onlist is fully OpenAI-compatible, all standard endpoints work:
196
+
197
+ ```python
198
+ # Embeddings
199
+ embedding = client.embeddings.create(
200
+ model="openai/text-embedding-3-small",
201
+ input="Hello world",
202
+ )
203
+
204
+ # Image generation
205
+ image = client.images.generate(
206
+ model="openai/gpt-image-2",
207
+ prompt="A sunset over Tokyo",
208
+ )
209
+
210
+ # Text-to-speech
211
+ audio = client.audio.speech.create(
212
+ model="openai/tts-1",
213
+ voice="alloy",
214
+ input="Welcome to Onlist.",
215
+ )
216
+ ```
217
+
218
+ ## Error Handling
219
+
220
+ For OpenAI-compatible API calls (`chat.completions`, `embeddings`, etc.), the
221
+ standard `openai` exceptions are raised:
222
+
223
+ ```python
224
+ import openai
225
+ from onlist import Onlist
226
+
227
+ client = Onlist(api_key="sk-...")
228
+
229
+ try:
230
+ response = client.chat.completions.create(
231
+ model="openai/gpt-4o",
232
+ messages=[{"role": "user", "content": "Hello"}],
233
+ )
234
+ except openai.AuthenticationError:
235
+ print("Invalid API key")
236
+ except openai.RateLimitError as e:
237
+ print(f"Rate limited: {e.message}")
238
+ ```
239
+
240
+ For marketplace API calls (`client.marketplace.*`), Onlist-specific exceptions
241
+ are raised:
242
+
243
+ ```python
244
+ from onlist import Onlist, AuthenticationError, APIError
245
+
246
+ client = Onlist(api_key="sk-...")
247
+
248
+ try:
249
+ providers = client.marketplace.providers.list()
250
+ except AuthenticationError:
251
+ print("Invalid API key for marketplace")
252
+ except APIError as e:
253
+ print(f"API error {e.status_code}: {e.message}")
254
+ ```
255
+
256
+ ## Migrate from OpenAI or OpenRouter
257
+
258
+ Already using the OpenAI SDK? Change one line:
259
+
260
+ ```diff
261
+ - from openai import OpenAI
262
+ - client = OpenAI(api_key="sk-...")
263
+ + from onlist import Onlist
264
+ + client = Onlist(api_key="sk-...")
265
+ ```
266
+
267
+ Or, if you prefer to keep using `openai` directly:
268
+
269
+ ```python
270
+ from openai import OpenAI
271
+
272
+ client = OpenAI(
273
+ api_key="your-onlist-key",
274
+ base_url="https://onlist.io/v1",
275
+ )
276
+ ```
277
+
278
+ ## Routing Metadata
279
+
280
+ Onlist returns routing information in response headers. Access them to see
281
+ which provider actually served your request:
282
+
283
+ ```python
284
+ # Use the with_raw_response pattern from the openai SDK:
285
+ raw_response = client.chat.completions.with_raw_response.create(
286
+ model="openai/gpt-4o",
287
+ messages=[{"role": "user", "content": "Hello"}],
288
+ )
289
+ print(raw_response.headers.get("x-onlist-route-id"))
290
+ print(raw_response.headers.get("x-onlist-provider"))
291
+
292
+ # Parse the completion as usual:
293
+ response = raw_response.parse()
294
+ print(response.choices[0].message.content)
295
+ ```
296
+
297
+ ## Links
298
+
299
+ - [Onlist Website](https://onlist.io)
300
+ - [API Documentation](https://onlist.io/docs)
301
+ - [Model Catalog](https://onlist.io/models)
302
+ - [Provider Directory](https://onlist.io/providers)
303
+ - [GitHub](https://github.com/OnlistTeam/onlist-python)
@@ -0,0 +1,16 @@
1
+ onlist/__init__.py,sha256=kKbL6LBo8VaGTQRygaXWPpoknNL_H_OmDN71gneURJY,949
2
+ onlist/_client.py,sha256=Y1lIHfZuMGXjAPsWnfXw0YahvgGtUSGw3F21qa93hLw,3733
3
+ onlist/_constants.py,sha256=ThkP4jJqW9aURURnJBtKxypfA-CB6u47iNGkQ14p0Bc,108
4
+ onlist/_exceptions.py,sha256=3qXyAVj5AxJL1dtw6LMMOu5QzovbZSkRj2xfs5N0Xuk,2996
5
+ onlist/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
6
+ onlist/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ onlist/resources/__init__.py,sha256=7pwEzUUXM64ryWYxPYHmikpxSz9gcNKiZNPEGsCTiQ0,118
8
+ onlist/resources/marketplace.py,sha256=R60TErHfQfv6aPUF8pEQ48bbn4R-X6SxFR8JOwxlGE0,6880
9
+ onlist/types/__init__.py,sha256=dob6SJt64cTyxMvlpz0vpRdSrxfQTxbPSvQ7jsB7_iU,522
10
+ onlist/types/model.py,sha256=PjxnXemrtx_Tn1HsrkW5nVolLDvj_auOJ7ucg5DeL2M,2236
11
+ onlist/types/provider.py,sha256=ZA1u6wMY7w-fKae5ozJ80krcZtIHgwfJf-Nq2aOhWb0,1759
12
+ onlist/types/routing.py,sha256=_ik0H2MhJP25XEjlcDuODfE-Z0p9R3XmyDazs3vUnpg,1065
13
+ onlist-0.1.0.dist-info/METADATA,sha256=t8PcUGdnGlW5YT7CurnxyceEihVJqGNMyRdMSr4mn3w,8424
14
+ onlist-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
15
+ onlist-0.1.0.dist-info/licenses/LICENSE,sha256=jDzyf0B2yCzZDc_3lR6iOGgP6Va4q9YvmCSP5Q-RTkw,1075
16
+ onlist-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Onlist (onlist.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.