bigif 1.0.0__tar.gz
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.
- bigif-1.0.0/PKG-INFO +28 -0
- bigif-1.0.0/README.md +13 -0
- bigif-1.0.0/pyproject.toml +24 -0
- bigif-1.0.0/setup.cfg +4 -0
- bigif-1.0.0/src/bigif/__init__.py +16 -0
- bigif-1.0.0/src/bigif/_client.py +270 -0
- bigif-1.0.0/src/bigif/_errors.py +33 -0
- bigif-1.0.0/src/bigif/_types.py +146 -0
- bigif-1.0.0/src/bigif/py.typed +0 -0
- bigif-1.0.0/src/bigif.egg-info/PKG-INFO +28 -0
- bigif-1.0.0/src/bigif.egg-info/SOURCES.txt +13 -0
- bigif-1.0.0/src/bigif.egg-info/dependency_links.txt +1 -0
- bigif-1.0.0/src/bigif.egg-info/requires.txt +1 -0
- bigif-1.0.0/src/bigif.egg-info/top_level.txt +1 -0
- bigif-1.0.0/tests/test_client.py +87 -0
bigif-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bigif
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The official Python SDK for the Big If True API (Big, Disco and Ticker, OpenAI-compatible).
|
|
5
|
+
Author: Big If True
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://bigif.org/developers/
|
|
8
|
+
Project-URL: Documentation, https://bigif.org/developers/sdks/#python
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: httpx<1,>=0.25
|
|
15
|
+
|
|
16
|
+
# bigif
|
|
17
|
+
|
|
18
|
+
The official Python SDK for the [Big If True API](https://bigif.org/developers/): use Big, Disco and Ticker
|
|
19
|
+
from Python with streaming, function calling, images and typed errors. Requests use your Big If True credits.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from bigif import BigIfTrue
|
|
23
|
+
|
|
24
|
+
client = BigIfTrue() # reads BIGIF_API_KEY
|
|
25
|
+
print(client.ask("Hello!", model="big"))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See https://bigif.org/developers/sdks/#python for the full guide.
|
bigif-1.0.0/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# bigif
|
|
2
|
+
|
|
3
|
+
The official Python SDK for the [Big If True API](https://bigif.org/developers/): use Big, Disco and Ticker
|
|
4
|
+
from Python with streaming, function calling, images and typed errors. Requests use your Big If True credits.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from bigif import BigIfTrue
|
|
8
|
+
|
|
9
|
+
client = BigIfTrue() # reads BIGIF_API_KEY
|
|
10
|
+
print(client.ask("Hello!", model="big"))
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
See https://bigif.org/developers/sdks/#python for the full guide.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bigif"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "The official Python SDK for the Big If True API (Big, Disco and Ticker, OpenAI-compatible)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Big If True" }]
|
|
13
|
+
dependencies = ["httpx>=0.25,<1"]
|
|
14
|
+
classifiers = ["Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Typing :: Typed"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://bigif.org/developers/"
|
|
18
|
+
Documentation = "https://bigif.org/developers/sdks/#python"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
bigif = ["py.typed"]
|
bigif-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The official Python SDK for the Big If True API.
|
|
2
|
+
|
|
3
|
+
from bigif import BigIfTrue
|
|
4
|
+
client = BigIfTrue() # reads BIGIF_API_KEY
|
|
5
|
+
client.ask("Hello", model="big")
|
|
6
|
+
|
|
7
|
+
Sync (``BigIfTrue``) and async (``AsyncBigIfTrue``) clients share one shape:
|
|
8
|
+
``models``, ``chat.completions``, ``completions``, ``images`` and ``ask``.
|
|
9
|
+
Responses are ``APIObject`` values: dicts that also allow attribute access.
|
|
10
|
+
"""
|
|
11
|
+
from ._client import AsyncBigIfTrue, BigIfTrue
|
|
12
|
+
from ._errors import BigIfTrueError
|
|
13
|
+
from ._types import APIObject, AsyncStream, Image, Stream
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
__all__ = ["BigIfTrue", "AsyncBigIfTrue", "BigIfTrueError", "APIObject", "Stream", "AsyncStream", "Image", "__version__"]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._errors import BigIfTrueError
|
|
11
|
+
from ._types import APIObject, AsyncStream, Image, Stream
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://bigif.org/api/openai/v1"
|
|
14
|
+
RETRY_STATUSES = {408, 409, 429, 500, 502, 503, 504}
|
|
15
|
+
Messages = List[Dict[str, Any]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _headers(api_key: str, app_name: Optional[str]) -> Dict[str, str]:
|
|
19
|
+
from . import __version__
|
|
20
|
+
|
|
21
|
+
headers = {"Authorization": f"Bearer {api_key}", "User-Agent": f"bigif-python/{__version__}", "Accept": "application/json"}
|
|
22
|
+
if app_name:
|
|
23
|
+
headers["X-Title"] = app_name
|
|
24
|
+
return headers
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _delay(attempt: int, response: Optional[httpx.Response]) -> float:
|
|
28
|
+
after = response.headers.get("retry-after") if response is not None else None
|
|
29
|
+
if after and after.replace(".", "", 1).isdigit():
|
|
30
|
+
return min(float(after), 30.0)
|
|
31
|
+
return min(0.5 * 2 ** attempt, 8.0) + random.random() * 0.25
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _error(response: httpx.Response) -> BigIfTrueError:
|
|
35
|
+
try:
|
|
36
|
+
data = response.json()
|
|
37
|
+
except ValueError:
|
|
38
|
+
data = {"error": {"message": response.text[:500] or f"HTTP {response.status_code}"}}
|
|
39
|
+
return BigIfTrueError.from_response(response.status_code, data, response.headers.get("x-request-id", ""))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _out_of_quota(response: httpx.Response) -> bool:
|
|
43
|
+
"""A usage limit or empty credit balance won't clear by retrying."""
|
|
44
|
+
try:
|
|
45
|
+
return (response.json().get("error") or {}).get("type") == "insufficient_quota"
|
|
46
|
+
except (ValueError, AttributeError):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _clean(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
51
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _Base:
|
|
55
|
+
def __init__(self, api_key: Optional[str], base_url: Optional[str], timeout: float, max_retries: int, app_name: Optional[str]) -> None:
|
|
56
|
+
self.api_key = api_key or os.environ.get("BIGIF_API_KEY") or ""
|
|
57
|
+
if not self.api_key:
|
|
58
|
+
raise BigIfTrueError("No API key. Pass api_key= or set BIGIF_API_KEY. Create one at https://bigif.org/workspace/?view=api.")
|
|
59
|
+
self.base_url = (base_url or os.environ.get("BIGIF_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
60
|
+
self.timeout = timeout
|
|
61
|
+
self.max_retries = max_retries
|
|
62
|
+
self.app_name = app_name
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BigIfTrue(_Base):
|
|
66
|
+
"""The Big If True API, synchronously.
|
|
67
|
+
|
|
68
|
+
``BigIfTrue(api_key=None, base_url=None, timeout=600, max_retries=2,
|
|
69
|
+
app_name=None)``; the key defaults to ``BIGIF_API_KEY``. ``app_name`` is
|
|
70
|
+
shown as the app in your API activity log.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, api_key: Optional[str] = None, *, base_url: Optional[str] = None, timeout: float = 600.0,
|
|
74
|
+
max_retries: int = 2, app_name: Optional[str] = None, http_client: Optional[httpx.Client] = None) -> None:
|
|
75
|
+
super().__init__(api_key, base_url, timeout, max_retries, app_name)
|
|
76
|
+
self._http = http_client or httpx.Client(timeout=httpx.Timeout(timeout, connect=10.0))
|
|
77
|
+
self.models = _Models(self)
|
|
78
|
+
self.chat = _Chat(self)
|
|
79
|
+
self.completions = _Completions(self)
|
|
80
|
+
self.images = _Images(self)
|
|
81
|
+
|
|
82
|
+
def request(self, method: str, path: str, body: Optional[dict] = None, *, stream: bool = False) -> Any:
|
|
83
|
+
url = f"{self.base_url}{path}"
|
|
84
|
+
headers = _headers(self.api_key, self.app_name)
|
|
85
|
+
for attempt in range(self.max_retries + 1):
|
|
86
|
+
try:
|
|
87
|
+
request = self._http.build_request(method, url, json=body, headers=headers)
|
|
88
|
+
response = self._http.send(request, stream=stream)
|
|
89
|
+
except httpx.TransportError as exc:
|
|
90
|
+
if attempt < self.max_retries:
|
|
91
|
+
time.sleep(_delay(attempt, None))
|
|
92
|
+
continue
|
|
93
|
+
raise BigIfTrueError(f"Could not reach the Big If True API: {exc}") from exc
|
|
94
|
+
if response.status_code < 400:
|
|
95
|
+
return Stream(response) if stream else APIObject.wrap(response.json())
|
|
96
|
+
if stream:
|
|
97
|
+
response.read()
|
|
98
|
+
if response.status_code in RETRY_STATUSES and attempt < self.max_retries and not _out_of_quota(response):
|
|
99
|
+
response.close()
|
|
100
|
+
time.sleep(_delay(attempt, response))
|
|
101
|
+
continue
|
|
102
|
+
raise _error(response)
|
|
103
|
+
raise BigIfTrueError("The request could not be completed.")
|
|
104
|
+
|
|
105
|
+
def ask(self, prompt: str, *, model: str = "big", system: Optional[str] = None, **params: Any) -> str:
|
|
106
|
+
"""One question, one answer: the reply's text."""
|
|
107
|
+
messages: Messages = ([{"role": "system", "content": system}] if system else []) + [{"role": "user", "content": prompt}]
|
|
108
|
+
return self.chat.completions.create(model=model, messages=messages, **params).choices[0].message.content or ""
|
|
109
|
+
|
|
110
|
+
def openapi(self) -> APIObject:
|
|
111
|
+
"""The API's OpenAPI description."""
|
|
112
|
+
return self.request("GET", "/openapi.json")
|
|
113
|
+
|
|
114
|
+
def close(self) -> None:
|
|
115
|
+
self._http.close()
|
|
116
|
+
|
|
117
|
+
def __enter__(self) -> "BigIfTrue":
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def __exit__(self, *exc: Any) -> None:
|
|
121
|
+
self.close()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AsyncBigIfTrue(_Base):
|
|
125
|
+
"""The Big If True API, asynchronously (same methods, awaited)."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, api_key: Optional[str] = None, *, base_url: Optional[str] = None, timeout: float = 600.0,
|
|
128
|
+
max_retries: int = 2, app_name: Optional[str] = None, http_client: Optional[httpx.AsyncClient] = None) -> None:
|
|
129
|
+
super().__init__(api_key, base_url, timeout, max_retries, app_name)
|
|
130
|
+
self._http = http_client or httpx.AsyncClient(timeout=httpx.Timeout(timeout, connect=10.0))
|
|
131
|
+
self.models = _AsyncModels(self)
|
|
132
|
+
self.chat = _AsyncChat(self)
|
|
133
|
+
self.completions = _AsyncCompletions(self)
|
|
134
|
+
self.images = _AsyncImages(self)
|
|
135
|
+
|
|
136
|
+
async def request(self, method: str, path: str, body: Optional[dict] = None, *, stream: bool = False) -> Any:
|
|
137
|
+
import asyncio
|
|
138
|
+
|
|
139
|
+
url = f"{self.base_url}{path}"
|
|
140
|
+
headers = _headers(self.api_key, self.app_name)
|
|
141
|
+
for attempt in range(self.max_retries + 1):
|
|
142
|
+
try:
|
|
143
|
+
request = self._http.build_request(method, url, json=body, headers=headers)
|
|
144
|
+
response = await self._http.send(request, stream=stream)
|
|
145
|
+
except httpx.TransportError as exc:
|
|
146
|
+
if attempt < self.max_retries:
|
|
147
|
+
await asyncio.sleep(_delay(attempt, None))
|
|
148
|
+
continue
|
|
149
|
+
raise BigIfTrueError(f"Could not reach the Big If True API: {exc}") from exc
|
|
150
|
+
if response.status_code < 400:
|
|
151
|
+
return AsyncStream(response) if stream else APIObject.wrap(response.json())
|
|
152
|
+
if stream:
|
|
153
|
+
await response.aread()
|
|
154
|
+
if response.status_code in RETRY_STATUSES and attempt < self.max_retries and not _out_of_quota(response):
|
|
155
|
+
await response.aclose()
|
|
156
|
+
await asyncio.sleep(_delay(attempt, response))
|
|
157
|
+
continue
|
|
158
|
+
raise _error(response)
|
|
159
|
+
raise BigIfTrueError("The request could not be completed.")
|
|
160
|
+
|
|
161
|
+
async def ask(self, prompt: str, *, model: str = "big", system: Optional[str] = None, **params: Any) -> str:
|
|
162
|
+
messages: Messages = ([{"role": "system", "content": system}] if system else []) + [{"role": "user", "content": prompt}]
|
|
163
|
+
reply = await self.chat.completions.create(model=model, messages=messages, **params)
|
|
164
|
+
return reply.choices[0].message.content or ""
|
|
165
|
+
|
|
166
|
+
async def openapi(self) -> APIObject:
|
|
167
|
+
return await self.request("GET", "/openapi.json")
|
|
168
|
+
|
|
169
|
+
async def close(self) -> None:
|
|
170
|
+
await self._http.aclose()
|
|
171
|
+
|
|
172
|
+
async def __aenter__(self) -> "AsyncBigIfTrue":
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
176
|
+
await self.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _stream_body(params: Dict[str, Any], stream: bool) -> Dict[str, Any]:
|
|
180
|
+
body = _clean(params)
|
|
181
|
+
if stream:
|
|
182
|
+
body["stream"] = True
|
|
183
|
+
body.setdefault("stream_options", {"include_usage": True})
|
|
184
|
+
return body
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class _Models:
|
|
188
|
+
def __init__(self, client: BigIfTrue) -> None:
|
|
189
|
+
self._client = client
|
|
190
|
+
|
|
191
|
+
def list(self) -> List[APIObject]:
|
|
192
|
+
return self._client.request("GET", "/models").data
|
|
193
|
+
|
|
194
|
+
def retrieve(self, model: str) -> APIObject:
|
|
195
|
+
return self._client.request("GET", f"/models/{model}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _ChatCompletions:
|
|
199
|
+
def __init__(self, client: BigIfTrue) -> None:
|
|
200
|
+
self._client = client
|
|
201
|
+
|
|
202
|
+
def create(self, *, model: str, messages: Messages, stream: bool = False, **params: Any) -> Union[APIObject, Stream]:
|
|
203
|
+
"""POST /chat/completions. Any OpenAI field may be passed (tools,
|
|
204
|
+
response_format, top_p, seed...). With ``stream=True`` returns a Stream."""
|
|
205
|
+
body = _stream_body({"model": model, "messages": messages, **params}, stream)
|
|
206
|
+
return self._client.request("POST", "/chat/completions", body, stream=stream)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _Chat:
|
|
210
|
+
def __init__(self, client: BigIfTrue) -> None:
|
|
211
|
+
self.completions = _ChatCompletions(client)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class _Completions:
|
|
215
|
+
def __init__(self, client: BigIfTrue) -> None:
|
|
216
|
+
self._client = client
|
|
217
|
+
|
|
218
|
+
def create(self, *, model: str, prompt: str, stream: bool = False, **params: Any) -> Union[APIObject, Stream]:
|
|
219
|
+
body = _stream_body({"model": model, "prompt": prompt, **params}, stream)
|
|
220
|
+
return self._client.request("POST", "/completions", body, stream=stream)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class _Images:
|
|
224
|
+
def __init__(self, client: BigIfTrue) -> None:
|
|
225
|
+
self._client = client
|
|
226
|
+
|
|
227
|
+
def generate(self, prompt: str, **params: Any) -> Image:
|
|
228
|
+
return Image(self._client.request("POST", "/images/generations", _clean({"prompt": prompt, **params})))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class _AsyncModels:
|
|
232
|
+
def __init__(self, client: AsyncBigIfTrue) -> None:
|
|
233
|
+
self._client = client
|
|
234
|
+
|
|
235
|
+
async def list(self) -> List[APIObject]:
|
|
236
|
+
return (await self._client.request("GET", "/models")).data
|
|
237
|
+
|
|
238
|
+
async def retrieve(self, model: str) -> APIObject:
|
|
239
|
+
return await self._client.request("GET", f"/models/{model}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class _AsyncChatCompletions:
|
|
243
|
+
def __init__(self, client: AsyncBigIfTrue) -> None:
|
|
244
|
+
self._client = client
|
|
245
|
+
|
|
246
|
+
async def create(self, *, model: str, messages: Messages, stream: bool = False, **params: Any) -> Union[APIObject, AsyncStream]:
|
|
247
|
+
body = _stream_body({"model": model, "messages": messages, **params}, stream)
|
|
248
|
+
return await self._client.request("POST", "/chat/completions", body, stream=stream)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class _AsyncChat:
|
|
252
|
+
def __init__(self, client: AsyncBigIfTrue) -> None:
|
|
253
|
+
self.completions = _AsyncChatCompletions(client)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class _AsyncCompletions:
|
|
257
|
+
def __init__(self, client: AsyncBigIfTrue) -> None:
|
|
258
|
+
self._client = client
|
|
259
|
+
|
|
260
|
+
async def create(self, *, model: str, prompt: str, stream: bool = False, **params: Any) -> Union[APIObject, AsyncStream]:
|
|
261
|
+
body = _stream_body({"model": model, "prompt": prompt, **params}, stream)
|
|
262
|
+
return await self._client.request("POST", "/completions", body, stream=stream)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class _AsyncImages:
|
|
266
|
+
def __init__(self, client: AsyncBigIfTrue) -> None:
|
|
267
|
+
self._client = client
|
|
268
|
+
|
|
269
|
+
async def generate(self, prompt: str, **params: Any) -> Image:
|
|
270
|
+
return Image(await self._client.request("POST", "/images/generations", _clean({"prompt": prompt, **params})))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BigIfTrueError(Exception):
|
|
7
|
+
"""An error from the API (or the connection to it).
|
|
8
|
+
|
|
9
|
+
``status`` is the HTTP status (0 for a connection failure); ``type``,
|
|
10
|
+
``code`` and ``param`` come from the API's OpenAI-shaped error body.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, *, status: int = 0, type: str = "", code: Optional[str] = None,
|
|
14
|
+
param: Optional[str] = None, request_id: str = "", body: Any = None) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.message = message
|
|
17
|
+
self.status = status
|
|
18
|
+
self.type = type
|
|
19
|
+
self.code = code
|
|
20
|
+
self.param = param
|
|
21
|
+
self.request_id = request_id
|
|
22
|
+
self.body = body
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f"BigIfTrueError(status={self.status}, code={self.code!r}, message={self.message!r})"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_response(cls, status: int, data: Any, request_id: str = "") -> "BigIfTrueError":
|
|
29
|
+
error = data.get("error") if isinstance(data, dict) else None
|
|
30
|
+
if isinstance(error, dict):
|
|
31
|
+
return cls(str(error.get("message") or f"HTTP {status}"), status=status, type=str(error.get("type") or ""),
|
|
32
|
+
code=error.get("code"), param=error.get("param"), request_id=request_id, body=data)
|
|
33
|
+
return cls(f"HTTP {status}", status=status, request_id=request_id, body=data)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, AsyncIterator, Iterator, Optional
|
|
6
|
+
|
|
7
|
+
from ._errors import BigIfTrueError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIObject(dict):
|
|
11
|
+
"""A response object: a dict whose keys are also attributes."""
|
|
12
|
+
|
|
13
|
+
def __getattr__(self, name: str) -> Any:
|
|
14
|
+
try:
|
|
15
|
+
return self[name]
|
|
16
|
+
except KeyError:
|
|
17
|
+
if name.startswith("__"):
|
|
18
|
+
raise AttributeError(name) from None
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def wrap(cls, value: Any) -> Any:
|
|
23
|
+
if isinstance(value, dict):
|
|
24
|
+
return cls({k: cls.wrap(v) for k, v in value.items()})
|
|
25
|
+
if isinstance(value, list):
|
|
26
|
+
return [cls.wrap(v) for v in value]
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def text(self) -> str:
|
|
31
|
+
"""A chunk's or completion's text (content delta, message or legacy text)."""
|
|
32
|
+
choices = self.get("choices") or []
|
|
33
|
+
if not choices:
|
|
34
|
+
return ""
|
|
35
|
+
choice = choices[0]
|
|
36
|
+
delta = choice.get("delta") or {}
|
|
37
|
+
message = choice.get("message") or {}
|
|
38
|
+
return delta.get("content") or message.get("content") or choice.get("text") or ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _events(lines: Iterator[str]) -> Iterator[APIObject]:
|
|
42
|
+
for line in lines:
|
|
43
|
+
if not line.startswith("data:"):
|
|
44
|
+
continue
|
|
45
|
+
data = line[5:].strip()
|
|
46
|
+
if data == "[DONE]":
|
|
47
|
+
return
|
|
48
|
+
if not data:
|
|
49
|
+
continue
|
|
50
|
+
chunk = json.loads(data)
|
|
51
|
+
if isinstance(chunk, dict) and chunk.get("error"):
|
|
52
|
+
raise BigIfTrueError.from_response(200, chunk)
|
|
53
|
+
yield APIObject.wrap(chunk)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Stream:
|
|
57
|
+
"""Iterate a streamed completion chunk by chunk. ``usage`` holds the final
|
|
58
|
+
usage (when the server sends it) and ``text`` the text so far."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, response: Any) -> None:
|
|
61
|
+
self._response = response
|
|
62
|
+
self.usage: Optional[APIObject] = None
|
|
63
|
+
self.text = ""
|
|
64
|
+
self.request_id = response.headers.get("x-request-id", "")
|
|
65
|
+
|
|
66
|
+
def __iter__(self) -> Iterator[APIObject]:
|
|
67
|
+
try:
|
|
68
|
+
for chunk in _events(self._response.iter_lines()):
|
|
69
|
+
if chunk.get("usage"):
|
|
70
|
+
self.usage = chunk["usage"]
|
|
71
|
+
self.text += chunk.text
|
|
72
|
+
yield chunk
|
|
73
|
+
finally:
|
|
74
|
+
self._response.close()
|
|
75
|
+
|
|
76
|
+
def until_done(self) -> str:
|
|
77
|
+
for _ in self:
|
|
78
|
+
pass
|
|
79
|
+
return self.text
|
|
80
|
+
|
|
81
|
+
def close(self) -> None:
|
|
82
|
+
self._response.close()
|
|
83
|
+
|
|
84
|
+
def __enter__(self) -> "Stream":
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def __exit__(self, *exc: Any) -> None:
|
|
88
|
+
self.close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AsyncStream:
|
|
92
|
+
"""The async version of :class:`Stream` (``async for chunk in stream``)."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, response: Any) -> None:
|
|
95
|
+
self._response = response
|
|
96
|
+
self.usage: Optional[APIObject] = None
|
|
97
|
+
self.text = ""
|
|
98
|
+
self.request_id = response.headers.get("x-request-id", "")
|
|
99
|
+
|
|
100
|
+
async def __aiter__(self) -> AsyncIterator[APIObject]:
|
|
101
|
+
try:
|
|
102
|
+
async for line in self._response.aiter_lines():
|
|
103
|
+
for chunk in _events(iter([line])):
|
|
104
|
+
if chunk.get("usage"):
|
|
105
|
+
self.usage = chunk["usage"]
|
|
106
|
+
self.text += chunk.text
|
|
107
|
+
yield chunk
|
|
108
|
+
if line.strip() == "data: [DONE]":
|
|
109
|
+
return
|
|
110
|
+
finally:
|
|
111
|
+
await self._response.aclose()
|
|
112
|
+
|
|
113
|
+
async def until_done(self) -> str:
|
|
114
|
+
async for _ in self:
|
|
115
|
+
pass
|
|
116
|
+
return self.text
|
|
117
|
+
|
|
118
|
+
async def close(self) -> None:
|
|
119
|
+
await self._response.aclose()
|
|
120
|
+
|
|
121
|
+
async def __aenter__(self) -> "AsyncStream":
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
125
|
+
await self.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Image(APIObject):
|
|
129
|
+
"""A generated image: ``bytes`` (when returned inline), ``url``, ``save()``."""
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def bytes(self) -> Optional[bytes]:
|
|
133
|
+
item = (self.get("data") or [{}])[0]
|
|
134
|
+
return base64.b64decode(item["b64_json"]) if item.get("b64_json") else None
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def url(self) -> Optional[str]:
|
|
138
|
+
return (self.get("data") or [{}])[0].get("url")
|
|
139
|
+
|
|
140
|
+
def save(self, path: str) -> str:
|
|
141
|
+
content = self.bytes
|
|
142
|
+
if content is None:
|
|
143
|
+
raise BigIfTrueError("This image was returned as a URL; download it from image.url.")
|
|
144
|
+
with open(path, "wb") as handle:
|
|
145
|
+
handle.write(content)
|
|
146
|
+
return path
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bigif
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The official Python SDK for the Big If True API (Big, Disco and Ticker, OpenAI-compatible).
|
|
5
|
+
Author: Big If True
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://bigif.org/developers/
|
|
8
|
+
Project-URL: Documentation, https://bigif.org/developers/sdks/#python
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: httpx<1,>=0.25
|
|
15
|
+
|
|
16
|
+
# bigif
|
|
17
|
+
|
|
18
|
+
The official Python SDK for the [Big If True API](https://bigif.org/developers/): use Big, Disco and Ticker
|
|
19
|
+
from Python with streaming, function calling, images and typed errors. Requests use your Big If True credits.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from bigif import BigIfTrue
|
|
23
|
+
|
|
24
|
+
client = BigIfTrue() # reads BIGIF_API_KEY
|
|
25
|
+
print(client.ask("Hello!", model="big"))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See https://bigif.org/developers/sdks/#python for the full guide.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/bigif/__init__.py
|
|
4
|
+
src/bigif/_client.py
|
|
5
|
+
src/bigif/_errors.py
|
|
6
|
+
src/bigif/_types.py
|
|
7
|
+
src/bigif/py.typed
|
|
8
|
+
src/bigif.egg-info/PKG-INFO
|
|
9
|
+
src/bigif.egg-info/SOURCES.txt
|
|
10
|
+
src/bigif.egg-info/dependency_links.txt
|
|
11
|
+
src/bigif.egg-info/requires.txt
|
|
12
|
+
src/bigif.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx<1,>=0.25
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bigif
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""The bigif SDK against a mocked API: requests carry the key, app name and
|
|
2
|
+
OpenAI fields; responses read as attributes; streams yield chunks, text and
|
|
3
|
+
usage; tool-call chunks arrive; errors raise BigIfTrueError with the API's
|
|
4
|
+
code; retryable failures retry but a quota error does not; async mirrors sync;
|
|
5
|
+
images decode and save."""
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from bigif import AsyncBigIfTrue, BigIfTrue, BigIfTrueError
|
|
13
|
+
|
|
14
|
+
SSE = "".join(f"data: {json.dumps(c)}\n\n" for c in (
|
|
15
|
+
{"choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}}]},
|
|
16
|
+
{"choices": [{"index": 0, "delta": {"content": "Hel"}}]},
|
|
17
|
+
{"choices": [{"index": 0, "delta": {"content": "lo"}}]},
|
|
18
|
+
{"choices": [{"index": 0, "delta": {"tool_calls": [{"index": 0, "id": "c1", "function": {"name": "f", "arguments": "{}"}}]}}]},
|
|
19
|
+
{"choices": [], "usage": {"prompt_tokens": 3, "completion_tokens": 2, "bigif": {"credits": 1.5}}},
|
|
20
|
+
)) + "data: [DONE]\n\n"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handler(seen):
|
|
24
|
+
def respond(request: httpx.Request) -> httpx.Response:
|
|
25
|
+
body = json.loads(request.content or b"{}")
|
|
26
|
+
seen.append((request, body))
|
|
27
|
+
if request.url.path.endswith("/models"):
|
|
28
|
+
return httpx.Response(200, json={"object": "list", "data": [{"id": "big"}, {"id": "disco"}]})
|
|
29
|
+
if request.url.path.endswith("/images/generations"):
|
|
30
|
+
return httpx.Response(200, json={"created": 1, "data": [{"b64_json": base64.b64encode(b"png").decode()}]})
|
|
31
|
+
if body.get("model") == "broke":
|
|
32
|
+
return httpx.Response(429, json={"error": {"message": "Out of credits", "type": "insufficient_quota", "code": "insufficient_quota"}})
|
|
33
|
+
if body.get("model") == "flaky" and len(seen) < 2:
|
|
34
|
+
return httpx.Response(503, json={"error": {"message": "busy", "type": "server_error"}}, headers={"retry-after": "0"})
|
|
35
|
+
if body.get("stream"):
|
|
36
|
+
return httpx.Response(200, text=SSE, headers={"content-type": "text/event-stream", "x-request-id": "r1"})
|
|
37
|
+
return httpx.Response(200, json={"id": "x", "object": "chat.completion", "choices": [{"index": 0, "message": {"role": "assistant", "content": "Hi there"}, "finish_reason": "stop"}],
|
|
38
|
+
"usage": {"total_tokens": 5}, "bigif": {"credits": 0.5}})
|
|
39
|
+
return respond
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def client(seen, **kwargs):
|
|
43
|
+
return BigIfTrue(api_key="bit-test", app_name="Tests", http_client=httpx.Client(transport=httpx.MockTransport(handler(seen))), **kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_requests_responses_and_streams():
|
|
47
|
+
seen = []
|
|
48
|
+
api = client(seen)
|
|
49
|
+
assert api.ask("Hello", model="disco", system="Be brief.", top_p=0.5) == "Hi there"
|
|
50
|
+
request, body = seen[-1]
|
|
51
|
+
assert request.headers["authorization"] == "Bearer bit-test" and request.headers["x-title"] == "Tests"
|
|
52
|
+
assert str(request.url) == "https://bigif.org/api/openai/v1/chat/completions"
|
|
53
|
+
assert body == {"model": "disco", "messages": [{"role": "system", "content": "Be brief."}, {"role": "user", "content": "Hello"}], "top_p": 0.5}
|
|
54
|
+
reply = api.chat.completions.create(model="big", messages=[{"role": "user", "content": "x"}])
|
|
55
|
+
assert reply.choices[0].message.content == "Hi there" and reply.bigif.credits == 0.5 and reply.missing is None
|
|
56
|
+
stream = api.chat.completions.create(model="big", messages=[{"role": "user", "content": "x"}], stream=True)
|
|
57
|
+
chunks = list(stream)
|
|
58
|
+
assert seen[-1][1]["stream_options"] == {"include_usage": True} and stream.text == "Hello" and stream.usage.bigif.credits == 1.5
|
|
59
|
+
assert chunks[3].choices[0].delta.tool_calls[0].function.name == "f" and stream.request_id == "r1"
|
|
60
|
+
assert [m.id for m in api.models.list()] == ["big", "disco"]
|
|
61
|
+
assert api.images.generate("a cat").bytes == b"png"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_errors_and_retries(tmp_path, monkeypatch):
|
|
65
|
+
monkeypatch.setattr("bigif._client._delay", lambda *a: 0)
|
|
66
|
+
seen = []
|
|
67
|
+
api = client(seen)
|
|
68
|
+
with pytest.raises(BigIfTrueError) as error:
|
|
69
|
+
api.ask("x", model="broke")
|
|
70
|
+
assert error.value.status == 429 and error.value.code == "insufficient_quota" and len(seen) == 1
|
|
71
|
+
seen.clear()
|
|
72
|
+
assert api.ask("x", model="flaky") == "Hi there" and len(seen) == 2
|
|
73
|
+
image = api.images.generate("a dog")
|
|
74
|
+
assert open(image.save(str(tmp_path / "dog.png")), "rb").read() == b"png"
|
|
75
|
+
monkeypatch.delenv("BIGIF_API_KEY", raising=False)
|
|
76
|
+
with pytest.raises(BigIfTrueError):
|
|
77
|
+
BigIfTrue()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_async_client():
|
|
82
|
+
seen = []
|
|
83
|
+
async with AsyncBigIfTrue(api_key="bit-test", http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler(seen)))) as api:
|
|
84
|
+
assert await api.ask("Hello") == "Hi there"
|
|
85
|
+
stream = await api.chat.completions.create(model="big", messages=[{"role": "user", "content": "x"}], stream=True)
|
|
86
|
+
assert [c.text async for c in stream][1:3] == ["Hel", "lo"] and stream.usage.prompt_tokens == 3
|
|
87
|
+
assert [m.id for m in await api.models.list()] == ["big", "disco"]
|