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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ 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"]