Skip to content

Commit 633f939

Browse files
stainless-botRobertCraigie
authored andcommitted
feat(client): add support for passing in a httpx client
1 parent 8e99af9 commit 633f939

File tree

8 files changed

+515
-200
lines changed

8 files changed

+515
-200
lines changed

README.md

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
# OpenAI Python API Library
1+
# OpenAI Python API library
22

33
[![PyPI version](https://img.shields.io/pypi/v/openai.svg)](https://pypi.org/project/openai/)
44

55
The OpenAI Python library provides convenient access to the OpenAI REST API from any Python 3.7+
6-
application. It includes type definitions for all request params and response fields,
6+
application. The library includes type definitions for all request params and response fields,
77
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
88

99
## Documentation
1010

11-
The API documentation can be found [here](https://platform.openai.com/docs).
11+
The API documentation can be found at [https://platform.openai.com/docs](https://platform.openai.com/docs).
1212

1313
## Installation
1414

@@ -43,7 +43,7 @@ print(completion.choices)
4343
While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/)
4444
and adding `OPENAI_API_KEY="my api key"` to your `.env` file so that your API Key is not stored in source control.
4545

46-
## Async Usage
46+
## Async usage
4747

4848
Simply import `AsyncOpenAI` instead of `OpenAI` and use `await` with each API call:
4949

@@ -148,11 +148,11 @@ We recommend that you always instantiate a client (e.g., with `client = OpenAI()
148148
- It's harder to mock for testing purposes
149149
- It's not possible to control cleanup of network connections
150150

151-
## Using Types
151+
## Using types
152152

153-
Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev), which provide helper methods for things like serializing back into json ([v1](https://docs.pydantic.dev/1.10/usage/models/), [v2](https://docs.pydantic.dev/latest/usage/serialization/)). To get a dictionary, you can call `dict(model)`.
153+
Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev), which provide helper methods for things like serializing back into JSON ([v1](https://docs.pydantic.dev/1.10/usage/models/), [v2](https://docs.pydantic.dev/latest/usage/serialization/)). To get a dictionary, call `dict(model)`.
154154

155-
This helps provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `"basic"`.
155+
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
156156

157157
## Pagination
158158

@@ -273,10 +273,10 @@ await client.files.create(
273273

274274
## Handling errors
275275

276-
When the library is unable to connect to the API (e.g., due to network connection problems or a timeout), a subclass of `openai.APIConnectionError` is raised.
276+
When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `openai.APIConnectionError` is raised.
277277

278-
When the API returns a non-success status code (i.e., 4xx or 5xx
279-
response), a subclass of `openai.APIStatusError` will be raised, containing `status_code` and `response` properties.
278+
When the API returns a non-success status code (that is, 4xx or 5xx
279+
response), a subclass of `openai.APIStatusError` is raised, containing `status_code` and `response` properties.
280280

281281
All errors inherit from `openai.APIError`.
282282

@@ -316,11 +316,11 @@ Error codes are as followed:
316316

317317
### Retries
318318

319-
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
319+
Certain errors are automatically retried 2 times by default, with a short exponential backoff.
320320
Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict,
321-
429 Rate Limit, and >=500 Internal errors will all be retried by default.
321+
429 Rate Limit, and >=500 Internal errors are all retried by default.
322322

323-
You can use the `max_retries` option to configure or disable this:
323+
You can use the `max_retries` option to configure or disable retry settings:
324324

325325
```python
326326
from openai import OpenAI
@@ -345,8 +345,8 @@ client.with_options(max_retries=5).chat.completions.create(
345345

346346
### Timeouts
347347

348-
Requests time out after 10 minutes by default. You can configure this with a `timeout` option,
349-
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration):
348+
By default requests time out after 10 minutes. You can configure this with a `timeout` option,
349+
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
350350

351351
```python
352352
from openai import OpenAI
@@ -376,13 +376,13 @@ client.with_options(timeout=5 * 1000).chat.completions.create(
376376

377377
On timeout, an `APITimeoutError` is thrown.
378378

379-
Note that requests which time out will be [retried twice by default](#retries).
379+
Note that requests that time out are [retried twice by default](#retries).
380380

381381
## Advanced
382382

383383
### How to tell whether `None` means `null` or missing
384384

385-
In an API response, a field may be explicitly null, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`:
385+
In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`:
386386

387387
```py
388388
if response.my_field is None:
@@ -392,27 +392,30 @@ if response.my_field is None:
392392
print('Got json like {"my_field": null}.')
393393
```
394394

395-
### Configuring custom URLs, proxies, and transports
395+
### Configuring the HTTP client
396396

397-
You can configure the following keyword arguments when instantiating the client:
397+
You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including:
398+
399+
- Support for proxies
400+
- Custom transports
401+
- Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality
398402

399403
```python
400404
import httpx
401405
from openai import OpenAI
402406

403407
client = OpenAI(
404-
# Use a custom base URL
405408
base_url="http://my.test.server.example.com:8083",
406-
proxies="http://my.test.proxy.example.com",
407-
transport=httpx.HTTPTransport(local_address="0.0.0.0"),
409+
http_client=httpx.Client(
410+
proxies="http://my.test.proxy.example.com",
411+
transport=httpx.HTTPTransport(local_address="0.0.0.0"),
412+
),
408413
)
409414
```
410415

411-
See the httpx documentation for information about the [`proxies`](https://www.python-httpx.org/advanced/#http-proxying) and [`transport`](https://www.python-httpx.org/advanced/#custom-transports) keyword arguments.
412-
413416
### Managing HTTP resources
414417

415-
By default we will close the underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__) is called but you can also manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
418+
By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
416419

417420
## Versioning
418421

src/openai/__init__.py

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888

8989
import httpx as _httpx
9090

91-
from ._base_client import DEFAULT_LIMITS, DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES
91+
from ._base_client import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES
9292

9393
api_key: str | None = _os.environ.get("OPENAI_API_KEY")
9494

@@ -104,14 +104,7 @@
104104

105105
default_query: _t.Mapping[str, object] | None = None
106106

107-
# See httpx documentation for [custom transports](https://www.python-httpx.org/advanced/#custom-transports)
108-
transport: Transport | None = None
109-
110-
# See httpx documentation for [proxies](https://www.python-httpx.org/advanced/#http-proxying)
111-
proxies: ProxiesTypes | None = None
112-
113-
# See httpx documentation for [limits](https://www.python-httpx.org/advanced/#pool-limit-configuration)
114-
connection_pool_limits: _httpx.Limits = DEFAULT_LIMITS
107+
http_client: _httpx.Client | None = None
115108

116109

117110
class _ModuleClient(OpenAI):
@@ -141,15 +134,13 @@ def organization(self, value: str | None) -> None: # type: ignore
141134
@property
142135
def base_url(self) -> _httpx.URL:
143136
if base_url is not None:
144-
# mypy doesn't use the type from the setter
145-
self._client.base_url = base_url # type: ignore[assignment]
137+
return _httpx.URL(base_url)
146138

147-
return self._client.base_url
139+
return super().base_url
148140

149141
@base_url.setter
150142
def base_url(self, url: _httpx.URL | str) -> None:
151-
# mypy doesn't use the type from the setter
152-
self._client.base_url = url # type: ignore[assignment]
143+
super().base_url = url # type: ignore[misc]
153144

154145
@property # type: ignore
155146
def timeout(self) -> float | Timeout | None:
@@ -191,6 +182,16 @@ def _custom_query(self, value: _t.Mapping[str, object] | None) -> None: # type:
191182

192183
default_query = value
193184

185+
@property # type: ignore
186+
def _client(self) -> _httpx.Client:
187+
return http_client or super()._client
188+
189+
@_client.setter # type: ignore
190+
def _client(self, value: _httpx.Client) -> None: # type: ignore
191+
global http_client
192+
193+
http_client = value
194+
194195
def __del__(self) -> None:
195196
try:
196197
super().__del__()
@@ -204,14 +205,7 @@ def __del__(self) -> None:
204205
def _load_client() -> OpenAI: # type: ignore[reportUnusedFunction]
205206
global _client
206207

207-
if (
208-
_client is None
209-
# if these options have been changed then we need to rebuild
210-
# the underlying http client
211-
or _client._transport != transport
212-
or _client._proxies != proxies
213-
or _client._limits != connection_pool_limits
214-
):
208+
if _client is None:
215209
_client = _ModuleClient(
216210
api_key=api_key,
217211
organization=organization,
@@ -220,9 +214,7 @@ def _load_client() -> OpenAI: # type: ignore[reportUnusedFunction]
220214
max_retries=max_retries,
221215
default_headers=default_headers,
222216
default_query=default_query,
223-
transport=transport,
224-
proxies=proxies,
225-
connection_pool_limits=connection_pool_limits,
217+
http_client=http_client,
226218
)
227219
return _client
228220

0 commit comments

Comments
 (0)