openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
104e5a54609c6fd003701a0f6f52e56cdeb806c3

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

1384lines · modecode

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