openai/openai-python

Public

mirrored from https://github.com/openai/openai-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.7.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/test_client.py

1386lines · modeblame

08b8179aDavid Schnurr2 years ago1# File generated from our OpenAPI spec by Stainless.
2
3from __future__ import annotations
4
d052708aStainless Bot2 years ago5import gc
08b8179aDavid Schnurr2 years ago6import os
7import json
8import asyncio
9import inspect
d052708aStainless Bot2 years ago10import tracemalloc
31573844Stainless Bot2 years ago11from typing import Any, Union, cast
08b8179aDavid Schnurr2 years ago12from unittest import mock
13
14import httpx
15import pytest
16from respx import MockRouter
17from pydantic import ValidationError
18
19from openai import OpenAI, AsyncOpenAI, APIResponseValidationError
20from openai._client import OpenAI, AsyncOpenAI
21from openai._models import BaseModel, FinalRequestOptions
22from openai._streaming import Stream, AsyncStream
a47375b7Stainless Bot2 years ago23from openai._exceptions import OpenAIError, APIStatusError, APITimeoutError, APIResponseValidationError
24from openai._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options
08b8179aDavid Schnurr2 years ago25
0733934fStainless Bot2 years ago26from .utils import update_env
27
08b8179aDavid Schnurr2 years ago28base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
29api_key = "My API Key"
30
31
32def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]:
33request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
34url = httpx.URL(request.url)
35return dict(url.params)
36
37
ba4f7a97Stainless Bot2 years ago38def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
39return 0.1
7aad3405Stainless Bot2 years ago40
41
42def _get_open_connections(client: OpenAI | AsyncOpenAI) -> int:
43transport = client._client._transport
44assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
45
46pool = transport._pool
47return len(pool._requests)
48
49
08b8179aDavid Schnurr2 years ago50class TestOpenAI:
51client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
52
53@pytest.mark.respx(base_url=base_url)
54def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago55respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago56
57response = self.client.post("/foo", cast_to=httpx.Response)
58assert response.status_code == 200
59assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago60assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago61
62@pytest.mark.respx(base_url=base_url)
63def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
64respx_mock.post("/foo").mock(
65return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
66)
67
68response = self.client.post("/foo", cast_to=httpx.Response)
69assert response.status_code == 200
70assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago71assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago72
73def test_copy(self) -> None:
74copied = self.client.copy()
75assert id(copied) != id(self.client)
76
77copied = self.client.copy(api_key="another My API Key")
78assert copied.api_key == "another My API Key"
79assert self.client.api_key == "My API Key"
80
81def test_copy_default_options(self) -> None:
82# options that have a default are overridden correctly
83copied = self.client.copy(max_retries=7)
84assert copied.max_retries == 7
85assert self.client.max_retries == 2
86
87copied2 = copied.copy(max_retries=6)
88assert copied2.max_retries == 6
89assert copied.max_retries == 7
90
91# timeout
92assert isinstance(self.client.timeout, httpx.Timeout)
93copied = self.client.copy(timeout=None)
94assert copied.timeout is None
95assert isinstance(self.client.timeout, httpx.Timeout)
96
97def test_copy_default_headers(self) -> None:
98client = OpenAI(
99base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
100)
101assert client.default_headers["X-Foo"] == "bar"
102
103# does not override the already given value when not specified
104copied = client.copy()
105assert copied.default_headers["X-Foo"] == "bar"
106
107# merges already given headers
108copied = client.copy(default_headers={"X-Bar": "stainless"})
109assert copied.default_headers["X-Foo"] == "bar"
110assert copied.default_headers["X-Bar"] == "stainless"
111
112# uses new values for any already given headers
113copied = client.copy(default_headers={"X-Foo": "stainless"})
114assert copied.default_headers["X-Foo"] == "stainless"
115
116# set_default_headers
117
118# completely overrides already set values
119copied = client.copy(set_default_headers={})
120assert copied.default_headers.get("X-Foo") is None
121
122copied = client.copy(set_default_headers={"X-Bar": "Robert"})
123assert copied.default_headers["X-Bar"] == "Robert"
124
125with pytest.raises(
126ValueError,
127match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
128):
129client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
130
131def test_copy_default_query(self) -> None:
132client = OpenAI(
133base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
134)
135assert _get_params(client)["foo"] == "bar"
136
137# does not override the already given value when not specified
138copied = client.copy()
139assert _get_params(copied)["foo"] == "bar"
140
141# merges already given params
142copied = client.copy(default_query={"bar": "stainless"})
143params = _get_params(copied)
144assert params["foo"] == "bar"
145assert params["bar"] == "stainless"
146
147# uses new values for any already given headers
148copied = client.copy(default_query={"foo": "stainless"})
149assert _get_params(copied)["foo"] == "stainless"
150
151# set_default_query
152
153# completely overrides already set values
154copied = client.copy(set_default_query={})
155assert _get_params(copied) == {}
156
157copied = client.copy(set_default_query={"bar": "Robert"})
158assert _get_params(copied)["bar"] == "Robert"
159
160with pytest.raises(
161ValueError,
162# TODO: update
163match="`default_query` and `set_default_query` arguments are mutually exclusive",
164):
165client.copy(set_default_query={}, default_query={"foo": "Bar"})
166
167def test_copy_signature(self) -> None:
168# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
169init_signature = inspect.signature(
170# mypy doesn't like that we access the `__init__` property.
171self.client.__init__, # type: ignore[misc]
172)
173copy_signature = inspect.signature(self.client.copy)
174exclude_params = {"transport", "proxies", "_strict_response_validation"}
175
176for name in init_signature.parameters.keys():
177if name in exclude_params:
178continue
179
180copy_param = copy_signature.parameters.get(name)
181assert copy_param is not None, f"copy() signature is missing the {name} param"
182
d052708aStainless Bot2 years ago183def test_copy_build_request(self) -> None:
184options = FinalRequestOptions(method="get", url="/foo")
185
186def build_request(options: FinalRequestOptions) -> None:
187client = self.client.copy()
188client._build_request(options)
189
190# ensure that the machinery is warmed up before tracing starts.
191build_request(options)
192gc.collect()
193
194tracemalloc.start(1000)
195
196snapshot_before = tracemalloc.take_snapshot()
197
198ITERATIONS = 10
199for _ in range(ITERATIONS):
200build_request(options)
201
ce04ec28Stainless Bot2 years ago202gc.collect()
d052708aStainless Bot2 years ago203snapshot_after = tracemalloc.take_snapshot()
204
205tracemalloc.stop()
206
207def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
208if diff.count == 0:
209# Avoid false positives by considering only leaks (i.e. allocations that persist).
210return
211
212if diff.count % ITERATIONS != 0:
213# Avoid false positives by considering only leaks that appear per iteration.
214return
215
216for frame in diff.traceback:
217if any(
218frame.filename.endswith(fragment)
219for fragment in [
220# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
221#
222# removing the decorator fixes the leak for reasons we don't understand.
223"openai/_response.py",
224# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
225"openai/_compat.py",
226# Standard library leaks we don't care about.
227"/logging/__init__.py",
228]
229):
230return
231
232leaks.append(diff)
233
234leaks: list[tracemalloc.StatisticDiff] = []
235for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
236add_leak(leaks, diff)
237if leaks:
238for leak in leaks:
239print("MEMORY LEAK:", leak)
240for frame in leak.traceback:
241print(frame)
242raise AssertionError()
243
08b8179aDavid Schnurr2 years ago244def test_request_timeout(self) -> None:
245request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
246timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
247assert timeout == DEFAULT_TIMEOUT
248
249request = self.client._build_request(
250FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
251)
252timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
253assert timeout == httpx.Timeout(100.0)
254
255def test_client_timeout_option(self) -> None:
256client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0))
257
258request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
259timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
260assert timeout == httpx.Timeout(0)
261
262def test_http_client_timeout_option(self) -> None:
263# custom timeout given to the httpx client should be used
264with httpx.Client(timeout=None) as http_client:
265client = OpenAI(
266base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
267)
268
269request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
270timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
271assert timeout == httpx.Timeout(None)
272
273# no timeout given to the httpx client should not use the httpx default
274with httpx.Client() as http_client:
275client = OpenAI(
276base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
277)
278
279request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
280timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
281assert timeout == DEFAULT_TIMEOUT
282
283# explicitly passing the default timeout currently results in it being ignored
284with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
285client = OpenAI(
286base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
287)
288
289request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
290timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
291assert timeout == DEFAULT_TIMEOUT # our default
292
293def test_default_headers_option(self) -> None:
294client = OpenAI(
295base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
296)
297request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
298assert request.headers.get("x-foo") == "bar"
299assert request.headers.get("x-stainless-lang") == "python"
300
301client2 = OpenAI(
302base_url=base_url,
303api_key=api_key,
304_strict_response_validation=True,
305default_headers={
306"X-Foo": "stainless",
307"X-Stainless-Lang": "my-overriding-header",
308},
309)
310request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
311assert request.headers.get("x-foo") == "stainless"
312assert request.headers.get("x-stainless-lang") == "my-overriding-header"
313
314def test_validate_headers(self) -> None:
315client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
316request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
317assert request.headers.get("Authorization") == f"Bearer {api_key}"
318
e967f5a5Stainless Bot2 years ago319with pytest.raises(OpenAIError):
08b8179aDavid Schnurr2 years ago320client2 = OpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
321_ = client2
322
323def test_default_query_option(self) -> None:
324client = OpenAI(
325base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
326)
327request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
328url = httpx.URL(request.url)
329assert dict(url.params) == {"query_param": "bar"}
330
331request = client._build_request(
332FinalRequestOptions(
333method="get",
334url="/foo",
335params={"foo": "baz", "query_param": "overriden"},
336)
337)
338url = httpx.URL(request.url)
339assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
340
341def test_request_extra_json(self) -> None:
342request = self.client._build_request(
343FinalRequestOptions(
344method="post",
345url="/foo",
346json_data={"foo": "bar"},
347extra_json={"baz": False},
348),
349)
350data = json.loads(request.content.decode("utf-8"))
351assert data == {"foo": "bar", "baz": False}
352
353request = self.client._build_request(
354FinalRequestOptions(
355method="post",
356url="/foo",
357extra_json={"baz": False},
358),
359)
360data = json.loads(request.content.decode("utf-8"))
361assert data == {"baz": False}
362
363# `extra_json` takes priority over `json_data` when keys clash
364request = self.client._build_request(
365FinalRequestOptions(
366method="post",
367url="/foo",
368json_data={"foo": "bar", "baz": True},
369extra_json={"baz": None},
370),
371)
372data = json.loads(request.content.decode("utf-8"))
373assert data == {"foo": "bar", "baz": None}
374
375def test_request_extra_headers(self) -> None:
376request = self.client._build_request(
377FinalRequestOptions(
378method="post",
379url="/foo",
380**make_request_options(extra_headers={"X-Foo": "Foo"}),
381),
382)
383assert request.headers.get("X-Foo") == "Foo"
384
385# `extra_headers` takes priority over `default_headers` when keys clash
386request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
387FinalRequestOptions(
388method="post",
389url="/foo",
390**make_request_options(
391extra_headers={"X-Bar": "false"},
392),
393),
394)
395assert request.headers.get("X-Bar") == "false"
396
397def test_request_extra_query(self) -> None:
398request = self.client._build_request(
399FinalRequestOptions(
400method="post",
401url="/foo",
402**make_request_options(
403extra_query={"my_query_param": "Foo"},
404),
405),
406)
31573844Stainless Bot2 years ago407params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago408assert params == {"my_query_param": "Foo"}
409
410# if both `query` and `extra_query` are given, they are merged
411request = self.client._build_request(
412FinalRequestOptions(
413method="post",
414url="/foo",
415**make_request_options(
416query={"bar": "1"},
417extra_query={"foo": "2"},
418),
419),
420)
31573844Stainless Bot2 years ago421params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago422assert params == {"bar": "1", "foo": "2"}
423
424# `extra_query` takes priority over `query` when keys clash
425request = self.client._build_request(
426FinalRequestOptions(
427method="post",
428url="/foo",
429**make_request_options(
430query={"foo": "1"},
431extra_query={"foo": "2"},
432),
433),
434)
31573844Stainless Bot2 years ago435params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago436assert params == {"foo": "2"}
437
438@pytest.mark.respx(base_url=base_url)
439def test_basic_union_response(self, respx_mock: MockRouter) -> None:
440class Model1(BaseModel):
441name: str
442
443class Model2(BaseModel):
444foo: str
445
446respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
447
448response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
449assert isinstance(response, Model2)
450assert response.foo == "bar"
451
452@pytest.mark.respx(base_url=base_url)
453def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
454"""Union of objects with the same field name using a different type"""
455
456class Model1(BaseModel):
457foo: int
458
459class Model2(BaseModel):
460foo: str
461
462respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
463
464response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
465assert isinstance(response, Model2)
466assert response.foo == "bar"
467
468respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
469
470response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
471assert isinstance(response, Model1)
472assert response.foo == 1
473
c26014e2Stainless Bot2 years ago474@pytest.mark.respx(base_url=base_url)
475def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
476"""
477Response that sets Content-Type to something other than application/json but returns json data
478"""
479
480class Model(BaseModel):
481foo: int
482
483respx_mock.get("/foo").mock(
484return_value=httpx.Response(
485200,
486content=json.dumps({"foo": 2}),
487headers={"Content-Type": "application/text"},
488)
489)
490
491response = self.client.get("/foo", cast_to=Model)
492assert isinstance(response, Model)
493assert response.foo == 2
494
f6f38a9bStainless Bot2 years ago495def test_base_url_setter(self) -> None:
496client = OpenAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
497assert client.base_url == "https://example.com/from_init/"
498
499client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
500
501assert client.base_url == "https://example.com/from_setter/"
502
0733934fStainless Bot2 years ago503def test_base_url_env(self) -> None:
504with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
505client = OpenAI(api_key=api_key, _strict_response_validation=True)
506assert client.base_url == "http://localhost:5000/from/env/"
507
08b8179aDavid Schnurr2 years ago508@pytest.mark.parametrize(
509"client",
510[
511OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
512OpenAI(
513base_url="http://localhost:5000/custom/path/",
514api_key=api_key,
515_strict_response_validation=True,
516http_client=httpx.Client(),
517),
518],
519ids=["standard", "custom http client"],
520)
521def test_base_url_trailing_slash(self, client: OpenAI) -> None:
522request = client._build_request(
523FinalRequestOptions(
524method="post",
525url="/foo",
526json_data={"foo": "bar"},
527),
528)
529assert request.url == "http://localhost:5000/custom/path/foo"
530
531@pytest.mark.parametrize(
532"client",
533[
534OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
535OpenAI(
536base_url="http://localhost:5000/custom/path/",
537api_key=api_key,
538_strict_response_validation=True,
539http_client=httpx.Client(),
540),
541],
542ids=["standard", "custom http client"],
543)
544def test_base_url_no_trailing_slash(self, client: OpenAI) -> None:
545request = client._build_request(
546FinalRequestOptions(
547method="post",
548url="/foo",
549json_data={"foo": "bar"},
550),
551)
552assert request.url == "http://localhost:5000/custom/path/foo"
553
554@pytest.mark.parametrize(
555"client",
556[
557OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
558OpenAI(
559base_url="http://localhost:5000/custom/path/",
560api_key=api_key,
561_strict_response_validation=True,
562http_client=httpx.Client(),
563),
564],
565ids=["standard", "custom http client"],
566)
567def test_absolute_request_url(self, client: OpenAI) -> None:
568request = client._build_request(
569FinalRequestOptions(
570method="post",
571url="https://myapi.com/foo",
572json_data={"foo": "bar"},
573),
574)
575assert request.url == "https://myapi.com/foo"
576
577def test_copied_client_does_not_close_http(self) -> None:
578client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
579assert not client.is_closed()
580
581copied = client.copy()
582assert copied is not client
583
a7ebc260Stainless Bot2 years ago584del copied
08b8179aDavid Schnurr2 years ago585
586assert not client.is_closed()
587
588def test_client_context_manager(self) -> None:
589client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
590with client as c2:
591assert c2 is client
592assert not c2.is_closed()
593assert not client.is_closed()
594assert client.is_closed()
595
596@pytest.mark.respx(base_url=base_url)
597def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
598class Model(BaseModel):
599foo: str
600
601respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
602
603with pytest.raises(APIResponseValidationError) as exc:
604self.client.get("/foo", cast_to=Model)
605
606assert isinstance(exc.value.__cause__, ValidationError)
607
608@pytest.mark.respx(base_url=base_url)
609def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
610class Model(BaseModel):
611name: str
612
613respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
614
615response = self.client.post("/foo", cast_to=Model, stream=True)
616assert isinstance(response, Stream)
617
618@pytest.mark.respx(base_url=base_url)
619def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
620class Model(BaseModel):
621name: str
622
623respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
624
625strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
626
627with pytest.raises(APIResponseValidationError):
628strict_client.get("/foo", cast_to=Model)
629
630client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
631
632response = client.get("/foo", cast_to=Model)
633assert isinstance(response, str) # type: ignore[unreachable]
634
635@pytest.mark.parametrize(
636"remaining_retries,retry_after,timeout",
637[
638[3, "20", 20],
639[3, "0", 0.5],
640[3, "-10", 0.5],
641[3, "60", 60],
642[3, "61", 0.5],
643[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
644[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
645[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
646[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
647[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
648[3, "99999999999999999999999999999999999", 0.5],
649[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
650[3, "", 0.5],
651[2, "", 0.5 * 2.0],
652[1, "", 0.5 * 4.0],
653],
654)
655@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
656def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
657client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
658
659headers = httpx.Headers({"retry-after": retry_after})
660options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
661calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
662assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
663
ba4f7a97Stainless Bot2 years ago664@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
665@pytest.mark.respx(base_url=base_url)
666def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
667respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
668
669with pytest.raises(APITimeoutError):
670self.client.post(
671"/chat/completions",
672body=dict(
673messages=[
674{
675"role": "user",
676"content": "Say this is a test",
677}
678],
679model="gpt-3.5-turbo",
680),
681cast_to=httpx.Response,
682options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
683)
7aad3405Stainless Bot2 years ago684
685assert _get_open_connections(self.client) == 0
686
ba4f7a97Stainless Bot2 years ago687@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago688@pytest.mark.respx(base_url=base_url)
ba4f7a97Stainless Bot2 years ago689def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
690respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago691
ba4f7a97Stainless Bot2 years ago692with pytest.raises(APIStatusError):
693self.client.post(
694"/chat/completions",
695body=dict(
696messages=[
697{
698"role": "user",
699"content": "Say this is a test",
700}
701],
702model="gpt-3.5-turbo",
703),
704cast_to=httpx.Response,
705options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
7aad3405Stainless Bot2 years ago706)
707
ba4f7a97Stainless Bot2 years ago708assert _get_open_connections(self.client) == 0
7aad3405Stainless Bot2 years ago709
08b8179aDavid Schnurr2 years ago710
711class TestAsyncOpenAI:
712client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
713
714@pytest.mark.respx(base_url=base_url)
715@pytest.mark.asyncio
716async def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago717respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago718
719response = await self.client.post("/foo", cast_to=httpx.Response)
720assert response.status_code == 200
721assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago722assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago723
724@pytest.mark.respx(base_url=base_url)
725@pytest.mark.asyncio
726async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
727respx_mock.post("/foo").mock(
728return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
729)
730
731response = await self.client.post("/foo", cast_to=httpx.Response)
732assert response.status_code == 200
733assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago734assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago735
736def test_copy(self) -> None:
737copied = self.client.copy()
738assert id(copied) != id(self.client)
739
740copied = self.client.copy(api_key="another My API Key")
741assert copied.api_key == "another My API Key"
742assert self.client.api_key == "My API Key"
743
744def test_copy_default_options(self) -> None:
745# options that have a default are overridden correctly
746copied = self.client.copy(max_retries=7)
747assert copied.max_retries == 7
748assert self.client.max_retries == 2
749
750copied2 = copied.copy(max_retries=6)
751assert copied2.max_retries == 6
752assert copied.max_retries == 7
753
754# timeout
755assert isinstance(self.client.timeout, httpx.Timeout)
756copied = self.client.copy(timeout=None)
757assert copied.timeout is None
758assert isinstance(self.client.timeout, httpx.Timeout)
759
760def test_copy_default_headers(self) -> None:
761client = AsyncOpenAI(
762base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
763)
764assert client.default_headers["X-Foo"] == "bar"
765
766# does not override the already given value when not specified
767copied = client.copy()
768assert copied.default_headers["X-Foo"] == "bar"
769
770# merges already given headers
771copied = client.copy(default_headers={"X-Bar": "stainless"})
772assert copied.default_headers["X-Foo"] == "bar"
773assert copied.default_headers["X-Bar"] == "stainless"
774
775# uses new values for any already given headers
776copied = client.copy(default_headers={"X-Foo": "stainless"})
777assert copied.default_headers["X-Foo"] == "stainless"
778
779# set_default_headers
780
781# completely overrides already set values
782copied = client.copy(set_default_headers={})
783assert copied.default_headers.get("X-Foo") is None
784
785copied = client.copy(set_default_headers={"X-Bar": "Robert"})
786assert copied.default_headers["X-Bar"] == "Robert"
787
788with pytest.raises(
789ValueError,
790match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
791):
792client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
793
794def test_copy_default_query(self) -> None:
795client = AsyncOpenAI(
796base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
797)
798assert _get_params(client)["foo"] == "bar"
799
800# does not override the already given value when not specified
801copied = client.copy()
802assert _get_params(copied)["foo"] == "bar"
803
804# merges already given params
805copied = client.copy(default_query={"bar": "stainless"})
806params = _get_params(copied)
807assert params["foo"] == "bar"
808assert params["bar"] == "stainless"
809
810# uses new values for any already given headers
811copied = client.copy(default_query={"foo": "stainless"})
812assert _get_params(copied)["foo"] == "stainless"
813
814# set_default_query
815
816# completely overrides already set values
817copied = client.copy(set_default_query={})
818assert _get_params(copied) == {}
819
820copied = client.copy(set_default_query={"bar": "Robert"})
821assert _get_params(copied)["bar"] == "Robert"
822
823with pytest.raises(
824ValueError,
825# TODO: update
826match="`default_query` and `set_default_query` arguments are mutually exclusive",
827):
828client.copy(set_default_query={}, default_query={"foo": "Bar"})
829
830def test_copy_signature(self) -> None:
831# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
832init_signature = inspect.signature(
833# mypy doesn't like that we access the `__init__` property.
834self.client.__init__, # type: ignore[misc]
835)
836copy_signature = inspect.signature(self.client.copy)
837exclude_params = {"transport", "proxies", "_strict_response_validation"}
838
839for name in init_signature.parameters.keys():
840if name in exclude_params:
841continue
842
843copy_param = copy_signature.parameters.get(name)
844assert copy_param is not None, f"copy() signature is missing the {name} param"
845
d052708aStainless Bot2 years ago846def test_copy_build_request(self) -> None:
847options = FinalRequestOptions(method="get", url="/foo")
848
849def build_request(options: FinalRequestOptions) -> None:
850client = self.client.copy()
851client._build_request(options)
852
853# ensure that the machinery is warmed up before tracing starts.
854build_request(options)
855gc.collect()
856
857tracemalloc.start(1000)
858
859snapshot_before = tracemalloc.take_snapshot()
860
861ITERATIONS = 10
862for _ in range(ITERATIONS):
863build_request(options)
864
ce04ec28Stainless Bot2 years ago865gc.collect()
d052708aStainless Bot2 years ago866snapshot_after = tracemalloc.take_snapshot()
867
868tracemalloc.stop()
869
870def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
871if diff.count == 0:
872# Avoid false positives by considering only leaks (i.e. allocations that persist).
873return
874
875if diff.count % ITERATIONS != 0:
876# Avoid false positives by considering only leaks that appear per iteration.
877return
878
879for frame in diff.traceback:
880if any(
881frame.filename.endswith(fragment)
882for fragment in [
883# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
884#
885# removing the decorator fixes the leak for reasons we don't understand.
886"openai/_response.py",
887# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
888"openai/_compat.py",
889# Standard library leaks we don't care about.
890"/logging/__init__.py",
891]
892):
893return
894
895leaks.append(diff)
896
897leaks: list[tracemalloc.StatisticDiff] = []
898for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
899add_leak(leaks, diff)
900if leaks:
901for leak in leaks:
902print("MEMORY LEAK:", leak)
903for frame in leak.traceback:
904print(frame)
905raise AssertionError()
906
08b8179aDavid Schnurr2 years ago907async def test_request_timeout(self) -> None:
908request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
909timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
910assert timeout == DEFAULT_TIMEOUT
911
912request = self.client._build_request(
913FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
914)
915timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
916assert timeout == httpx.Timeout(100.0)
917
918async def test_client_timeout_option(self) -> None:
919client = AsyncOpenAI(
920base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
921)
922
923request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
924timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
925assert timeout == httpx.Timeout(0)
926
927async def test_http_client_timeout_option(self) -> None:
928# custom timeout given to the httpx client should be used
929async with httpx.AsyncClient(timeout=None) as http_client:
930client = AsyncOpenAI(
931base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
932)
933
934request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
935timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
936assert timeout == httpx.Timeout(None)
937
938# no timeout given to the httpx client should not use the httpx default
939async with httpx.AsyncClient() as http_client:
940client = AsyncOpenAI(
941base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
942)
943
944request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
945timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
946assert timeout == DEFAULT_TIMEOUT
947
948# explicitly passing the default timeout currently results in it being ignored
949async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
950client = AsyncOpenAI(
951base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
952)
953
954request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
955timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
956assert timeout == DEFAULT_TIMEOUT # our default
957
958def test_default_headers_option(self) -> None:
959client = AsyncOpenAI(
960base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
961)
962request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
963assert request.headers.get("x-foo") == "bar"
964assert request.headers.get("x-stainless-lang") == "python"
965
966client2 = AsyncOpenAI(
967base_url=base_url,
968api_key=api_key,
969_strict_response_validation=True,
970default_headers={
971"X-Foo": "stainless",
972"X-Stainless-Lang": "my-overriding-header",
973},
974)
975request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
976assert request.headers.get("x-foo") == "stainless"
977assert request.headers.get("x-stainless-lang") == "my-overriding-header"
978
979def test_validate_headers(self) -> None:
980client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
981request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
982assert request.headers.get("Authorization") == f"Bearer {api_key}"
983
e967f5a5Stainless Bot2 years ago984with pytest.raises(OpenAIError):
08b8179aDavid Schnurr2 years ago985client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
986_ = client2
987
988def test_default_query_option(self) -> None:
989client = AsyncOpenAI(
990base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
991)
992request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
993url = httpx.URL(request.url)
994assert dict(url.params) == {"query_param": "bar"}
995
996request = client._build_request(
997FinalRequestOptions(
998method="get",
999url="/foo",
1000params={"foo": "baz", "query_param": "overriden"},
1001)
1002)
1003url = httpx.URL(request.url)
1004assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
1005
1006def test_request_extra_json(self) -> None:
1007request = self.client._build_request(
1008FinalRequestOptions(
1009method="post",
1010url="/foo",
1011json_data={"foo": "bar"},
1012extra_json={"baz": False},
1013),
1014)
1015data = json.loads(request.content.decode("utf-8"))
1016assert data == {"foo": "bar", "baz": False}
1017
1018request = self.client._build_request(
1019FinalRequestOptions(
1020method="post",
1021url="/foo",
1022extra_json={"baz": False},
1023),
1024)
1025data = json.loads(request.content.decode("utf-8"))
1026assert data == {"baz": False}
1027
1028# `extra_json` takes priority over `json_data` when keys clash
1029request = self.client._build_request(
1030FinalRequestOptions(
1031method="post",
1032url="/foo",
1033json_data={"foo": "bar", "baz": True},
1034extra_json={"baz": None},
1035),
1036)
1037data = json.loads(request.content.decode("utf-8"))
1038assert data == {"foo": "bar", "baz": None}
1039
1040def test_request_extra_headers(self) -> None:
1041request = self.client._build_request(
1042FinalRequestOptions(
1043method="post",
1044url="/foo",
1045**make_request_options(extra_headers={"X-Foo": "Foo"}),
1046),
1047)
1048assert request.headers.get("X-Foo") == "Foo"
1049
1050# `extra_headers` takes priority over `default_headers` when keys clash
1051request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
1052FinalRequestOptions(
1053method="post",
1054url="/foo",
1055**make_request_options(
1056extra_headers={"X-Bar": "false"},
1057),
1058),
1059)
1060assert request.headers.get("X-Bar") == "false"
1061
1062def test_request_extra_query(self) -> None:
1063request = self.client._build_request(
1064FinalRequestOptions(
1065method="post",
1066url="/foo",
1067**make_request_options(
1068extra_query={"my_query_param": "Foo"},
1069),
1070),
1071)
31573844Stainless Bot2 years ago1072params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1073assert params == {"my_query_param": "Foo"}
1074
1075# if both `query` and `extra_query` are given, they are merged
1076request = self.client._build_request(
1077FinalRequestOptions(
1078method="post",
1079url="/foo",
1080**make_request_options(
1081query={"bar": "1"},
1082extra_query={"foo": "2"},
1083),
1084),
1085)
31573844Stainless Bot2 years ago1086params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1087assert params == {"bar": "1", "foo": "2"}
1088
1089# `extra_query` takes priority over `query` when keys clash
1090request = self.client._build_request(
1091FinalRequestOptions(
1092method="post",
1093url="/foo",
1094**make_request_options(
1095query={"foo": "1"},
1096extra_query={"foo": "2"},
1097),
1098),
1099)
31573844Stainless Bot2 years ago1100params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1101assert params == {"foo": "2"}
1102
1103@pytest.mark.respx(base_url=base_url)
1104async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
1105class Model1(BaseModel):
1106name: str
1107
1108class Model2(BaseModel):
1109foo: str
1110
1111respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1112
1113response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1114assert isinstance(response, Model2)
1115assert response.foo == "bar"
1116
1117@pytest.mark.respx(base_url=base_url)
1118async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
1119"""Union of objects with the same field name using a different type"""
1120
1121class Model1(BaseModel):
1122foo: int
1123
1124class Model2(BaseModel):
1125foo: str
1126
1127respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1128
1129response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1130assert isinstance(response, Model2)
1131assert response.foo == "bar"
1132
1133respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1134
1135response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1136assert isinstance(response, Model1)
1137assert response.foo == 1
1138
c26014e2Stainless Bot2 years ago1139@pytest.mark.respx(base_url=base_url)
1140async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
1141"""
1142Response that sets Content-Type to something other than application/json but returns json data
1143"""
1144
1145class Model(BaseModel):
1146foo: int
1147
1148respx_mock.get("/foo").mock(
1149return_value=httpx.Response(
1150200,
1151content=json.dumps({"foo": 2}),
1152headers={"Content-Type": "application/text"},
1153)
1154)
1155
1156response = await self.client.get("/foo", cast_to=Model)
1157assert isinstance(response, Model)
1158assert response.foo == 2
1159
f6f38a9bStainless Bot2 years ago1160def test_base_url_setter(self) -> None:
1161client = AsyncOpenAI(
1162base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1163)
1164assert client.base_url == "https://example.com/from_init/"
1165
1166client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1167
1168assert client.base_url == "https://example.com/from_setter/"
1169
0733934fStainless Bot2 years ago1170def test_base_url_env(self) -> None:
1171with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1172client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1173assert client.base_url == "http://localhost:5000/from/env/"
1174
08b8179aDavid Schnurr2 years ago1175@pytest.mark.parametrize(
1176"client",
1177[
1178AsyncOpenAI(
1179base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1180),
1181AsyncOpenAI(
1182base_url="http://localhost:5000/custom/path/",
1183api_key=api_key,
1184_strict_response_validation=True,
1185http_client=httpx.AsyncClient(),
1186),
1187],
1188ids=["standard", "custom http client"],
1189)
1190def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
1191request = client._build_request(
1192FinalRequestOptions(
1193method="post",
1194url="/foo",
1195json_data={"foo": "bar"},
1196),
1197)
1198assert request.url == "http://localhost:5000/custom/path/foo"
1199
1200@pytest.mark.parametrize(
1201"client",
1202[
1203AsyncOpenAI(
1204base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1205),
1206AsyncOpenAI(
1207base_url="http://localhost:5000/custom/path/",
1208api_key=api_key,
1209_strict_response_validation=True,
1210http_client=httpx.AsyncClient(),
1211),
1212],
1213ids=["standard", "custom http client"],
1214)
1215def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
1216request = client._build_request(
1217FinalRequestOptions(
1218method="post",
1219url="/foo",
1220json_data={"foo": "bar"},
1221),
1222)
1223assert request.url == "http://localhost:5000/custom/path/foo"
1224
1225@pytest.mark.parametrize(
1226"client",
1227[
1228AsyncOpenAI(
1229base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1230),
1231AsyncOpenAI(
1232base_url="http://localhost:5000/custom/path/",
1233api_key=api_key,
1234_strict_response_validation=True,
1235http_client=httpx.AsyncClient(),
1236),
1237],
1238ids=["standard", "custom http client"],
1239)
1240def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
1241request = client._build_request(
1242FinalRequestOptions(
1243method="post",
1244url="https://myapi.com/foo",
1245json_data={"foo": "bar"},
1246),
1247)
1248assert request.url == "https://myapi.com/foo"
1249
1250async def test_copied_client_does_not_close_http(self) -> None:
1251client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1252assert not client.is_closed()
1253
1254copied = client.copy()
1255assert copied is not client
1256
a7ebc260Stainless Bot2 years ago1257del copied
08b8179aDavid Schnurr2 years ago1258
1259await asyncio.sleep(0.2)
1260assert not client.is_closed()
1261
1262async def test_client_context_manager(self) -> None:
1263client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1264async with client as c2:
1265assert c2 is client
1266assert not c2.is_closed()
1267assert not client.is_closed()
1268assert client.is_closed()
1269
1270@pytest.mark.respx(base_url=base_url)
1271@pytest.mark.asyncio
1272async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
1273class Model(BaseModel):
1274foo: str
1275
1276respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1277
1278with pytest.raises(APIResponseValidationError) as exc:
1279await self.client.get("/foo", cast_to=Model)
1280
1281assert isinstance(exc.value.__cause__, ValidationError)
1282
1283@pytest.mark.respx(base_url=base_url)
1284@pytest.mark.asyncio
1285async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
1286class Model(BaseModel):
1287name: str
1288
1289respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1290
1291response = await self.client.post("/foo", cast_to=Model, stream=True)
1292assert isinstance(response, AsyncStream)
1293
1294@pytest.mark.respx(base_url=base_url)
1295@pytest.mark.asyncio
1296async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1297class Model(BaseModel):
1298name: str
1299
1300respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1301
1302strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1303
1304with pytest.raises(APIResponseValidationError):
1305await strict_client.get("/foo", cast_to=Model)
1306
1307client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
1308
1309response = await client.get("/foo", cast_to=Model)
1310assert isinstance(response, str) # type: ignore[unreachable]
1311
1312@pytest.mark.parametrize(
1313"remaining_retries,retry_after,timeout",
1314[
1315[3, "20", 20],
1316[3, "0", 0.5],
1317[3, "-10", 0.5],
1318[3, "60", 60],
1319[3, "61", 0.5],
1320[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1321[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1322[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1323[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1324[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1325[3, "99999999999999999999999999999999999", 0.5],
1326[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1327[3, "", 0.5],
1328[2, "", 0.5 * 2.0],
1329[1, "", 0.5 * 4.0],
1330],
1331)
1332@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
1333@pytest.mark.asyncio
1334async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
1335client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1336
1337headers = httpx.Headers({"retry-after": retry_after})
1338options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
1339calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
1340assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
7aad3405Stainless Bot2 years ago1341
ba4f7a97Stainless Bot2 years ago1342@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1343@pytest.mark.respx(base_url=base_url)
1344async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
1345respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
1346
1347with pytest.raises(APITimeoutError):
1348await self.client.post(
1349"/chat/completions",
1350body=dict(
1351messages=[
1352{
1353"role": "user",
1354"content": "Say this is a test",
1355}
1356],
1357model="gpt-3.5-turbo",
1358),
1359cast_to=httpx.Response,
1360options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
1361)
7aad3405Stainless Bot2 years ago1362
1363assert _get_open_connections(self.client) == 0
1364
ba4f7a97Stainless Bot2 years ago1365@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago1366@pytest.mark.respx(base_url=base_url)
ba4f7a97Stainless Bot2 years ago1367async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
1368respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago1369
ba4f7a97Stainless Bot2 years ago1370with pytest.raises(APIStatusError):
1371await self.client.post(
1372"/chat/completions",
1373body=dict(
1374messages=[
1375{
1376"role": "user",
1377"content": "Say this is a test",
1378}
1379],
1380model="gpt-3.5-turbo",
1381),
1382cast_to=httpx.Response,
1383options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
7aad3405Stainless Bot2 years ago1384)
1385
ba4f7a97Stainless Bot2 years ago1386assert _get_open_connections(self.client) == 0