Skip to content

Commit ae64c05

Browse files
stainless-app[bot]stainless-bot
authored andcommitted
fix(client/async): avoid blocking io call for platform headers (#1488)
1 parent 6aa2a80 commit ae64c05

File tree

4 files changed

+40
-5
lines changed

4 files changed

+40
-5
lines changed

src/openai/_base_client.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
RequestOptions,
6161
ModelBuilderProtocol,
6262
)
63-
from ._utils import is_dict, is_list, is_given, lru_cache, is_mapping
63+
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
6464
from ._compat import model_copy, model_dump
6565
from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
6666
from ._response import (
@@ -359,6 +359,7 @@ def __init__(
359359
self._custom_query = custom_query or {}
360360
self._strict_response_validation = _strict_response_validation
361361
self._idempotency_header = None
362+
self._platform: Platform | None = None
362363

363364
if max_retries is None: # pyright: ignore[reportUnnecessaryComparison]
364365
raise TypeError(
@@ -623,7 +624,10 @@ def base_url(self, url: URL | str) -> None:
623624
self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url))
624625

625626
def platform_headers(self) -> Dict[str, str]:
626-
return platform_headers(self._version)
627+
# the actual implementation is in a separate `lru_cache` decorated
628+
# function because adding `lru_cache` to methods will leak memory
629+
# https://github.com/python/cpython/issues/88476
630+
return platform_headers(self._version, platform=self._platform)
627631

628632
def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None:
629633
"""Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified.
@@ -1513,6 +1517,11 @@ async def _request(
15131517
stream_cls: type[_AsyncStreamT] | None,
15141518
remaining_retries: int | None,
15151519
) -> ResponseT | _AsyncStreamT:
1520+
if self._platform is None:
1521+
# `get_platform` can make blocking IO calls so we
1522+
# execute it earlier while we are in an async context
1523+
self._platform = await asyncify(get_platform)()
1524+
15161525
cast_to = self._maybe_override_cast_to(cast_to, options)
15171526
await self._prepare_options(options)
15181527

@@ -1949,11 +1958,11 @@ def get_platform() -> Platform:
19491958

19501959

19511960
@lru_cache(maxsize=None)
1952-
def platform_headers(version: str) -> Dict[str, str]:
1961+
def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]:
19531962
return {
19541963
"X-Stainless-Lang": "python",
19551964
"X-Stainless-Package-Version": version,
1956-
"X-Stainless-OS": str(get_platform()),
1965+
"X-Stainless-OS": str(platform or get_platform()),
19571966
"X-Stainless-Arch": str(get_architecture()),
19581967
"X-Stainless-Runtime": get_python_runtime(),
19591968
"X-Stainless-Runtime-Version": get_python_version(),

src/openai/_utils/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@
4949
maybe_transform as maybe_transform,
5050
async_maybe_transform as async_maybe_transform,
5151
)
52+
from ._reflection import function_has_argument as function_has_argument

src/openai/_utils/_reflection.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import inspect
2+
from typing import Any, Callable
3+
4+
5+
def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool:
6+
"""Returns whether or not the given function has a specific parameter"""
7+
sig = inspect.signature(func)
8+
return arg_name in sig.parameters

src/openai/_utils/_sync.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import anyio
88
import anyio.to_thread
99

10+
from ._reflection import function_has_argument
11+
1012
T_Retval = TypeVar("T_Retval")
1113
T_ParamSpec = ParamSpec("T_ParamSpec")
1214

@@ -59,6 +61,21 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
5961

6062
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
6163
partial_f = functools.partial(function, *args, **kwargs)
62-
return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter)
64+
65+
# In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old
66+
# `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid
67+
# surfacing deprecation warnings.
68+
if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"):
69+
return await anyio.to_thread.run_sync(
70+
partial_f,
71+
abandon_on_cancel=cancellable,
72+
limiter=limiter,
73+
)
74+
75+
return await anyio.to_thread.run_sync(
76+
partial_f,
77+
cancellable=cancellable,
78+
limiter=limiter,
79+
)
6380

6481
return wrapper

0 commit comments

Comments
 (0)