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 +48 -0
- onlist/_client.py +125 -0
- onlist/_constants.py +3 -0
- onlist/_exceptions.py +106 -0
- onlist/_version.py +1 -0
- onlist/py.typed +0 -0
- onlist/resources/__init__.py +3 -0
- onlist/resources/marketplace.py +230 -0
- onlist/types/__init__.py +28 -0
- onlist/types/model.py +84 -0
- onlist/types/provider.py +71 -0
- onlist/types/routing.py +39 -0
- onlist-0.1.0.dist-info/METADATA +303 -0
- onlist-0.1.0.dist-info/RECORD +16 -0
- onlist-0.1.0.dist-info/WHEEL +4 -0
- onlist-0.1.0.dist-info/licenses/LICENSE +21 -0
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
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,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()
|
onlist/types/__init__.py
ADDED
|
@@ -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"}
|
onlist/types/provider.py
ADDED
|
@@ -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"}
|
onlist/types/routing.py
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/onlist/)
|
|
44
|
+
[](https://pypi.org/project/onlist/)
|
|
45
|
+
[](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,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.
|