openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.3.7

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

1402lines · 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_setter(self) -> None:
449 client = OpenAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
450 assert client.base_url == "https://example.com/from_init/"
451
452 client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
453
454 assert client.base_url == "https://example.com/from_setter/"
455
456 def test_base_url_env(self) -> None:
457 with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
458 client = OpenAI(api_key=api_key, _strict_response_validation=True)
459 assert client.base_url == "http://localhost:5000/from/env/"
460
461 @pytest.mark.parametrize(
462 "client",
463 [
464 OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
465 OpenAI(
466 base_url="http://localhost:5000/custom/path/",
467 api_key=api_key,
468 _strict_response_validation=True,
469 http_client=httpx.Client(),
470 ),
471 ],
472 ids=["standard", "custom http client"],
473 )
474 def test_base_url_trailing_slash(self, client: OpenAI) -> None:
475 request = client._build_request(
476 FinalRequestOptions(
477 method="post",
478 url="/foo",
479 json_data={"foo": "bar"},
480 ),
481 )
482 assert request.url == "http://localhost:5000/custom/path/foo"
483
484 @pytest.mark.parametrize(
485 "client",
486 [
487 OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
488 OpenAI(
489 base_url="http://localhost:5000/custom/path/",
490 api_key=api_key,
491 _strict_response_validation=True,
492 http_client=httpx.Client(),
493 ),
494 ],
495 ids=["standard", "custom http client"],
496 )
497 def test_base_url_no_trailing_slash(self, client: OpenAI) -> None:
498 request = client._build_request(
499 FinalRequestOptions(
500 method="post",
501 url="/foo",
502 json_data={"foo": "bar"},
503 ),
504 )
505 assert request.url == "http://localhost:5000/custom/path/foo"
506
507 @pytest.mark.parametrize(
508 "client",
509 [
510 OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
511 OpenAI(
512 base_url="http://localhost:5000/custom/path/",
513 api_key=api_key,
514 _strict_response_validation=True,
515 http_client=httpx.Client(),
516 ),
517 ],
518 ids=["standard", "custom http client"],
519 )
520 def test_absolute_request_url(self, client: OpenAI) -> None:
521 request = client._build_request(
522 FinalRequestOptions(
523 method="post",
524 url="https://myapi.com/foo",
525 json_data={"foo": "bar"},
526 ),
527 )
528 assert request.url == "https://myapi.com/foo"
529
530 def test_client_del(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 client.__del__()
535
536 assert client.is_closed()
537
538 def test_copied_client_does_not_close_http(self) -> None:
539 client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
540 assert not client.is_closed()
541
542 copied = client.copy()
543 assert copied is not client
544
545 copied.__del__()
546
547 assert not copied.is_closed()
548 assert not client.is_closed()
549
550 def test_client_context_manager(self) -> None:
551 client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
552 with client as c2:
553 assert c2 is client
554 assert not c2.is_closed()
555 assert not client.is_closed()
556 assert client.is_closed()
557
558 @pytest.mark.respx(base_url=base_url)
559 def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
560 class Model(BaseModel):
561 foo: str
562
563 respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
564
565 with pytest.raises(APIResponseValidationError) as exc:
566 self.client.get("/foo", cast_to=Model)
567
568 assert isinstance(exc.value.__cause__, ValidationError)
569
570 @pytest.mark.respx(base_url=base_url)
571 def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
572 class Model(BaseModel):
573 name: str
574
575 respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
576
577 response = self.client.post("/foo", cast_to=Model, stream=True)
578 assert isinstance(response, Stream)
579
580 @pytest.mark.respx(base_url=base_url)
581 def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
582 class Model(BaseModel):
583 name: str
584
585 respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
586
587 strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
588
589 with pytest.raises(APIResponseValidationError):
590 strict_client.get("/foo", cast_to=Model)
591
592 client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
593
594 response = client.get("/foo", cast_to=Model)
595 assert isinstance(response, str) # type: ignore[unreachable]
596
597 @pytest.mark.parametrize(
598 "remaining_retries,retry_after,timeout",
599 [
600 [3, "20", 20],
601 [3, "0", 0.5],
602 [3, "-10", 0.5],
603 [3, "60", 60],
604 [3, "61", 0.5],
605 [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
606 [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
607 [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
608 [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
609 [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
610 [3, "99999999999999999999999999999999999", 0.5],
611 [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
612 [3, "", 0.5],
613 [2, "", 0.5 * 2.0],
614 [1, "", 0.5 * 4.0],
615 ],
616 )
617 @mock.patch("time.time", mock.MagicMock(return_value=1696004797))
618 def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
619 client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
620
621 headers = httpx.Headers({"retry-after": retry_after})
622 options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
623 calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
624 assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
625
626 @mock.patch("httpx.Response.__init__", _low_retry_response_init)
627 def test_retrying_timeout_errors_doesnt_leak(self) -> None:
628 def raise_for_status(response: httpx.Response) -> None:
629 raise httpx.TimeoutException("Test timeout error", request=response.request)
630
631 with mock.patch("httpx.Response.raise_for_status", raise_for_status):
632 with pytest.raises(APITimeoutError):
633 self.client.post(
634 "/chat/completions",
635 body=dict(
636 messages=[
637 {
638 "role": "user",
639 "content": "Say this is a test",
640 }
641 ],
642 model="gpt-3.5-turbo",
643 ),
644 cast_to=httpx.Response,
645 options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
646 )
647
648 assert _get_open_connections(self.client) == 0
649
650 @mock.patch("httpx.Response.__init__", _low_retry_response_init)
651 def test_retrying_runtime_errors_doesnt_leak(self) -> None:
652 def raise_for_status(_response: httpx.Response) -> None:
653 raise RuntimeError("Test error")
654
655 with mock.patch("httpx.Response.raise_for_status", raise_for_status):
656 with pytest.raises(APIConnectionError):
657 self.client.post(
658 "/chat/completions",
659 body=dict(
660 messages=[
661 {
662 "role": "user",
663 "content": "Say this is a test",
664 }
665 ],
666 model="gpt-3.5-turbo",
667 ),
668 cast_to=httpx.Response,
669 options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
670 )
671
672 assert _get_open_connections(self.client) == 0
673
674 @mock.patch("httpx.Response.__init__", _low_retry_response_init)
675 def test_retrying_status_errors_doesnt_leak(self) -> None:
676 def raise_for_status(response: httpx.Response) -> None:
677 response.status_code = 500
678 raise httpx.HTTPStatusError("Test 500 error", response=response, request=response.request)
679
680 with mock.patch("httpx.Response.raise_for_status", raise_for_status):
681 with pytest.raises(APIStatusError):
682 self.client.post(
683 "/chat/completions",
684 body=dict(
685 messages=[
686 {
687 "role": "user",
688 "content": "Say this is a test",
689 }
690 ],
691 model="gpt-3.5-turbo",
692 ),
693 cast_to=httpx.Response,
694 options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
695 )
696
697 assert _get_open_connections(self.client) == 0
698
699 @pytest.mark.respx(base_url=base_url)
700 def test_status_error_within_httpx(self, respx_mock: MockRouter) -> None:
701 respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
702
703 def on_response(response: httpx.Response) -> None:
704 raise httpx.HTTPStatusError(
705 "Simulating an error inside httpx",
706 response=response,
707 request=response.request,
708 )
709
710 client = OpenAI(
711 base_url=base_url,
712 api_key=api_key,
713 _strict_response_validation=True,
714 http_client=httpx.Client(
715 event_hooks={
716 "response": [on_response],
717 }
718 ),
719 max_retries=0,
720 )
721 with pytest.raises(APIStatusError):
722 client.post("/foo", cast_to=httpx.Response)
723
724
725class TestAsyncOpenAI:
726 client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
727
728 @pytest.mark.respx(base_url=base_url)
729 @pytest.mark.asyncio
730 async def test_raw_response(self, respx_mock: MockRouter) -> None:
731 respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
732
733 response = await self.client.post("/foo", cast_to=httpx.Response)
734 assert response.status_code == 200
735 assert isinstance(response, httpx.Response)
736 assert response.json() == {"foo": "bar"}
737
738 @pytest.mark.respx(base_url=base_url)
739 @pytest.mark.asyncio
740 async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
741 respx_mock.post("/foo").mock(
742 return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
743 )
744
745 response = await self.client.post("/foo", cast_to=httpx.Response)
746 assert response.status_code == 200
747 assert isinstance(response, httpx.Response)
748 assert response.json() == {"foo": "bar"}
749
750 def test_copy(self) -> None:
751 copied = self.client.copy()
752 assert id(copied) != id(self.client)
753
754 copied = self.client.copy(api_key="another My API Key")
755 assert copied.api_key == "another My API Key"
756 assert self.client.api_key == "My API Key"
757
758 def test_copy_default_options(self) -> None:
759 # options that have a default are overridden correctly
760 copied = self.client.copy(max_retries=7)
761 assert copied.max_retries == 7
762 assert self.client.max_retries == 2
763
764 copied2 = copied.copy(max_retries=6)
765 assert copied2.max_retries == 6
766 assert copied.max_retries == 7
767
768 # timeout
769 assert isinstance(self.client.timeout, httpx.Timeout)
770 copied = self.client.copy(timeout=None)
771 assert copied.timeout is None
772 assert isinstance(self.client.timeout, httpx.Timeout)
773
774 def test_copy_default_headers(self) -> None:
775 client = AsyncOpenAI(
776 base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
777 )
778 assert client.default_headers["X-Foo"] == "bar"
779
780 # does not override the already given value when not specified
781 copied = client.copy()
782 assert copied.default_headers["X-Foo"] == "bar"
783
784 # merges already given headers
785 copied = client.copy(default_headers={"X-Bar": "stainless"})
786 assert copied.default_headers["X-Foo"] == "bar"
787 assert copied.default_headers["X-Bar"] == "stainless"
788
789 # uses new values for any already given headers
790 copied = client.copy(default_headers={"X-Foo": "stainless"})
791 assert copied.default_headers["X-Foo"] == "stainless"
792
793 # set_default_headers
794
795 # completely overrides already set values
796 copied = client.copy(set_default_headers={})
797 assert copied.default_headers.get("X-Foo") is None
798
799 copied = client.copy(set_default_headers={"X-Bar": "Robert"})
800 assert copied.default_headers["X-Bar"] == "Robert"
801
802 with pytest.raises(
803 ValueError,
804 match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
805 ):
806 client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
807
808 def test_copy_default_query(self) -> None:
809 client = AsyncOpenAI(
810 base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
811 )
812 assert _get_params(client)["foo"] == "bar"
813
814 # does not override the already given value when not specified
815 copied = client.copy()
816 assert _get_params(copied)["foo"] == "bar"
817
818 # merges already given params
819 copied = client.copy(default_query={"bar": "stainless"})
820 params = _get_params(copied)
821 assert params["foo"] == "bar"
822 assert params["bar"] == "stainless"
823
824 # uses new values for any already given headers
825 copied = client.copy(default_query={"foo": "stainless"})
826 assert _get_params(copied)["foo"] == "stainless"
827
828 # set_default_query
829
830 # completely overrides already set values
831 copied = client.copy(set_default_query={})
832 assert _get_params(copied) == {}
833
834 copied = client.copy(set_default_query={"bar": "Robert"})
835 assert _get_params(copied)["bar"] == "Robert"
836
837 with pytest.raises(
838 ValueError,
839 # TODO: update
840 match="`default_query` and `set_default_query` arguments are mutually exclusive",
841 ):
842 client.copy(set_default_query={}, default_query={"foo": "Bar"})
843
844 def test_copy_signature(self) -> None:
845 # ensure the same parameters that can be passed to the client are defined in the `.copy()` method
846 init_signature = inspect.signature(
847 # mypy doesn't like that we access the `__init__` property.
848 self.client.__init__, # type: ignore[misc]
849 )
850 copy_signature = inspect.signature(self.client.copy)
851 exclude_params = {"transport", "proxies", "_strict_response_validation"}
852
853 for name in init_signature.parameters.keys():
854 if name in exclude_params:
855 continue
856
857 copy_param = copy_signature.parameters.get(name)
858 assert copy_param is not None, f"copy() signature is missing the {name} param"
859
860 async def test_request_timeout(self) -> None:
861 request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
862 timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
863 assert timeout == DEFAULT_TIMEOUT
864
865 request = self.client._build_request(
866 FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
867 )
868 timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
869 assert timeout == httpx.Timeout(100.0)
870
871 async def test_client_timeout_option(self) -> None:
872 client = AsyncOpenAI(
873 base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
874 )
875
876 request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
877 timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
878 assert timeout == httpx.Timeout(0)
879
880 async def test_http_client_timeout_option(self) -> None:
881 # custom timeout given to the httpx client should be used
882 async with httpx.AsyncClient(timeout=None) as http_client:
883 client = AsyncOpenAI(
884 base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
885 )
886
887 request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
888 timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
889 assert timeout == httpx.Timeout(None)
890
891 # no timeout given to the httpx client should not use the httpx default
892 async with httpx.AsyncClient() as http_client:
893 client = AsyncOpenAI(
894 base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
895 )
896
897 request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
898 timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
899 assert timeout == DEFAULT_TIMEOUT
900
901 # explicitly passing the default timeout currently results in it being ignored
902 async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
903 client = AsyncOpenAI(
904 base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
905 )
906
907 request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
908 timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
909 assert timeout == DEFAULT_TIMEOUT # our default
910
911 def test_default_headers_option(self) -> None:
912 client = AsyncOpenAI(
913 base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
914 )
915 request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
916 assert request.headers.get("x-foo") == "bar"
917 assert request.headers.get("x-stainless-lang") == "python"
918
919 client2 = AsyncOpenAI(
920 base_url=base_url,
921 api_key=api_key,
922 _strict_response_validation=True,
923 default_headers={
924 "X-Foo": "stainless",
925 "X-Stainless-Lang": "my-overriding-header",
926 },
927 )
928 request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
929 assert request.headers.get("x-foo") == "stainless"
930 assert request.headers.get("x-stainless-lang") == "my-overriding-header"
931
932 def test_validate_headers(self) -> None:
933 client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
934 request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
935 assert request.headers.get("Authorization") == f"Bearer {api_key}"
936
937 with pytest.raises(Exception):
938 client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
939 _ = client2
940
941 def test_default_query_option(self) -> None:
942 client = AsyncOpenAI(
943 base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
944 )
945 request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
946 url = httpx.URL(request.url)
947 assert dict(url.params) == {"query_param": "bar"}
948
949 request = client._build_request(
950 FinalRequestOptions(
951 method="get",
952 url="/foo",
953 params={"foo": "baz", "query_param": "overriden"},
954 )
955 )
956 url = httpx.URL(request.url)
957 assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
958
959 def test_request_extra_json(self) -> None:
960 request = self.client._build_request(
961 FinalRequestOptions(
962 method="post",
963 url="/foo",
964 json_data={"foo": "bar"},
965 extra_json={"baz": False},
966 ),
967 )
968 data = json.loads(request.content.decode("utf-8"))
969 assert data == {"foo": "bar", "baz": False}
970
971 request = self.client._build_request(
972 FinalRequestOptions(
973 method="post",
974 url="/foo",
975 extra_json={"baz": False},
976 ),
977 )
978 data = json.loads(request.content.decode("utf-8"))
979 assert data == {"baz": False}
980
981 # `extra_json` takes priority over `json_data` when keys clash
982 request = self.client._build_request(
983 FinalRequestOptions(
984 method="post",
985 url="/foo",
986 json_data={"foo": "bar", "baz": True},
987 extra_json={"baz": None},
988 ),
989 )
990 data = json.loads(request.content.decode("utf-8"))
991 assert data == {"foo": "bar", "baz": None}
992
993 def test_request_extra_headers(self) -> None:
994 request = self.client._build_request(
995 FinalRequestOptions(
996 method="post",
997 url="/foo",
998 **make_request_options(extra_headers={"X-Foo": "Foo"}),
999 ),
1000 )
1001 assert request.headers.get("X-Foo") == "Foo"
1002
1003 # `extra_headers` takes priority over `default_headers` when keys clash
1004 request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
1005 FinalRequestOptions(
1006 method="post",
1007 url="/foo",
1008 **make_request_options(
1009 extra_headers={"X-Bar": "false"},
1010 ),
1011 ),
1012 )
1013 assert request.headers.get("X-Bar") == "false"
1014
1015 def test_request_extra_query(self) -> None:
1016 request = self.client._build_request(
1017 FinalRequestOptions(
1018 method="post",
1019 url="/foo",
1020 **make_request_options(
1021 extra_query={"my_query_param": "Foo"},
1022 ),
1023 ),
1024 )
1025 params = cast(Dict[str, str], dict(request.url.params))
1026 assert params == {"my_query_param": "Foo"}
1027
1028 # if both `query` and `extra_query` are given, they are merged
1029 request = self.client._build_request(
1030 FinalRequestOptions(
1031 method="post",
1032 url="/foo",
1033 **make_request_options(
1034 query={"bar": "1"},
1035 extra_query={"foo": "2"},
1036 ),
1037 ),
1038 )
1039 params = cast(Dict[str, str], dict(request.url.params))
1040 assert params == {"bar": "1", "foo": "2"}
1041
1042 # `extra_query` takes priority over `query` when keys clash
1043 request = self.client._build_request(
1044 FinalRequestOptions(
1045 method="post",
1046 url="/foo",
1047 **make_request_options(
1048 query={"foo": "1"},
1049 extra_query={"foo": "2"},
1050 ),
1051 ),
1052 )
1053 params = cast(Dict[str, str], dict(request.url.params))
1054 assert params == {"foo": "2"}
1055
1056 @pytest.mark.respx(base_url=base_url)
1057 async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
1058 class Model1(BaseModel):
1059 name: str
1060
1061 class Model2(BaseModel):
1062 foo: str
1063
1064 respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1065
1066 response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1067 assert isinstance(response, Model2)
1068 assert response.foo == "bar"
1069
1070 @pytest.mark.respx(base_url=base_url)
1071 async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
1072 """Union of objects with the same field name using a different type"""
1073
1074 class Model1(BaseModel):
1075 foo: int
1076
1077 class Model2(BaseModel):
1078 foo: str
1079
1080 respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1081
1082 response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1083 assert isinstance(response, Model2)
1084 assert response.foo == "bar"
1085
1086 respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1087
1088 response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1089 assert isinstance(response, Model1)
1090 assert response.foo == 1
1091
1092 @pytest.mark.respx(base_url=base_url)
1093 async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
1094 """
1095 Response that sets Content-Type to something other than application/json but returns json data
1096 """
1097
1098 class Model(BaseModel):
1099 foo: int
1100
1101 respx_mock.get("/foo").mock(
1102 return_value=httpx.Response(
1103 200,
1104 content=json.dumps({"foo": 2}),
1105 headers={"Content-Type": "application/text"},
1106 )
1107 )
1108
1109 response = await self.client.get("/foo", cast_to=Model)
1110 assert isinstance(response, Model)
1111 assert response.foo == 2
1112
1113 def test_base_url_setter(self) -> None:
1114 client = AsyncOpenAI(
1115 base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1116 )
1117 assert client.base_url == "https://example.com/from_init/"
1118
1119 client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1120
1121 assert client.base_url == "https://example.com/from_setter/"
1122
1123 def test_base_url_env(self) -> None:
1124 with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1125 client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1126 assert client.base_url == "http://localhost:5000/from/env/"
1127
1128 @pytest.mark.parametrize(
1129 "client",
1130 [
1131 AsyncOpenAI(
1132 base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1133 ),
1134 AsyncOpenAI(
1135 base_url="http://localhost:5000/custom/path/",
1136 api_key=api_key,
1137 _strict_response_validation=True,
1138 http_client=httpx.AsyncClient(),
1139 ),
1140 ],
1141 ids=["standard", "custom http client"],
1142 )
1143 def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
1144 request = client._build_request(
1145 FinalRequestOptions(
1146 method="post",
1147 url="/foo",
1148 json_data={"foo": "bar"},
1149 ),
1150 )
1151 assert request.url == "http://localhost:5000/custom/path/foo"
1152
1153 @pytest.mark.parametrize(
1154 "client",
1155 [
1156 AsyncOpenAI(
1157 base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1158 ),
1159 AsyncOpenAI(
1160 base_url="http://localhost:5000/custom/path/",
1161 api_key=api_key,
1162 _strict_response_validation=True,
1163 http_client=httpx.AsyncClient(),
1164 ),
1165 ],
1166 ids=["standard", "custom http client"],
1167 )
1168 def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
1169 request = client._build_request(
1170 FinalRequestOptions(
1171 method="post",
1172 url="/foo",
1173 json_data={"foo": "bar"},
1174 ),
1175 )
1176 assert request.url == "http://localhost:5000/custom/path/foo"
1177
1178 @pytest.mark.parametrize(
1179 "client",
1180 [
1181 AsyncOpenAI(
1182 base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1183 ),
1184 AsyncOpenAI(
1185 base_url="http://localhost:5000/custom/path/",
1186 api_key=api_key,
1187 _strict_response_validation=True,
1188 http_client=httpx.AsyncClient(),
1189 ),
1190 ],
1191 ids=["standard", "custom http client"],
1192 )
1193 def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
1194 request = client._build_request(
1195 FinalRequestOptions(
1196 method="post",
1197 url="https://myapi.com/foo",
1198 json_data={"foo": "bar"},
1199 ),
1200 )
1201 assert request.url == "https://myapi.com/foo"
1202
1203 async def test_client_del(self) -> None:
1204 client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1205 assert not client.is_closed()
1206
1207 client.__del__()
1208
1209 await asyncio.sleep(0.2)
1210 assert client.is_closed()
1211
1212 async def test_copied_client_does_not_close_http(self) -> None:
1213 client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1214 assert not client.is_closed()
1215
1216 copied = client.copy()
1217 assert copied is not client
1218
1219 copied.__del__()
1220
1221 await asyncio.sleep(0.2)
1222 assert not copied.is_closed()
1223 assert not client.is_closed()
1224
1225 async def test_client_context_manager(self) -> None:
1226 client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1227 async with client as c2:
1228 assert c2 is client
1229 assert not c2.is_closed()
1230 assert not client.is_closed()
1231 assert client.is_closed()
1232
1233 @pytest.mark.respx(base_url=base_url)
1234 @pytest.mark.asyncio
1235 async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
1236 class Model(BaseModel):
1237 foo: str
1238
1239 respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1240
1241 with pytest.raises(APIResponseValidationError) as exc:
1242 await self.client.get("/foo", cast_to=Model)
1243
1244 assert isinstance(exc.value.__cause__, ValidationError)
1245
1246 @pytest.mark.respx(base_url=base_url)
1247 @pytest.mark.asyncio
1248 async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
1249 class Model(BaseModel):
1250 name: str
1251
1252 respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1253
1254 response = await self.client.post("/foo", cast_to=Model, stream=True)
1255 assert isinstance(response, AsyncStream)
1256
1257 @pytest.mark.respx(base_url=base_url)
1258 @pytest.mark.asyncio
1259 async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1260 class Model(BaseModel):
1261 name: str
1262
1263 respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1264
1265 strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1266
1267 with pytest.raises(APIResponseValidationError):
1268 await strict_client.get("/foo", cast_to=Model)
1269
1270 client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
1271
1272 response = await client.get("/foo", cast_to=Model)
1273 assert isinstance(response, str) # type: ignore[unreachable]
1274
1275 @pytest.mark.parametrize(
1276 "remaining_retries,retry_after,timeout",
1277 [
1278 [3, "20", 20],
1279 [3, "0", 0.5],
1280 [3, "-10", 0.5],
1281 [3, "60", 60],
1282 [3, "61", 0.5],
1283 [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1284 [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1285 [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1286 [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1287 [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1288 [3, "99999999999999999999999999999999999", 0.5],
1289 [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1290 [3, "", 0.5],
1291 [2, "", 0.5 * 2.0],
1292 [1, "", 0.5 * 4.0],
1293 ],
1294 )
1295 @mock.patch("time.time", mock.MagicMock(return_value=1696004797))
1296 @pytest.mark.asyncio
1297 async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
1298 client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1299
1300 headers = httpx.Headers({"retry-after": retry_after})
1301 options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
1302 calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
1303 assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
1304
1305 @mock.patch("httpx.Response.__init__", _low_retry_response_init)
1306 async def test_retrying_timeout_errors_doesnt_leak(self) -> None:
1307 def raise_for_status(response: httpx.Response) -> None:
1308 raise httpx.TimeoutException("Test timeout error", request=response.request)
1309
1310 with mock.patch("httpx.Response.raise_for_status", raise_for_status):
1311 with pytest.raises(APITimeoutError):
1312 await self.client.post(
1313 "/chat/completions",
1314 body=dict(
1315 messages=[
1316 {
1317 "role": "user",
1318 "content": "Say this is a test",
1319 }
1320 ],
1321 model="gpt-3.5-turbo",
1322 ),
1323 cast_to=httpx.Response,
1324 options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
1325 )
1326
1327 assert _get_open_connections(self.client) == 0
1328
1329 @mock.patch("httpx.Response.__init__", _low_retry_response_init)
1330 async def test_retrying_runtime_errors_doesnt_leak(self) -> None:
1331 def raise_for_status(_response: httpx.Response) -> None:
1332 raise RuntimeError("Test error")
1333
1334 with mock.patch("httpx.Response.raise_for_status", raise_for_status):
1335 with pytest.raises(APIConnectionError):
1336 await self.client.post(
1337 "/chat/completions",
1338 body=dict(
1339 messages=[
1340 {
1341 "role": "user",
1342 "content": "Say this is a test",
1343 }
1344 ],
1345 model="gpt-3.5-turbo",
1346 ),
1347 cast_to=httpx.Response,
1348 options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
1349 )
1350
1351 assert _get_open_connections(self.client) == 0
1352
1353 @mock.patch("httpx.Response.__init__", _low_retry_response_init)
1354 async def test_retrying_status_errors_doesnt_leak(self) -> None:
1355 def raise_for_status(response: httpx.Response) -> None:
1356 response.status_code = 500
1357 raise httpx.HTTPStatusError("Test 500 error", response=response, request=response.request)
1358
1359 with mock.patch("httpx.Response.raise_for_status", raise_for_status):
1360 with pytest.raises(APIStatusError):
1361 await self.client.post(
1362 "/chat/completions",
1363 body=dict(
1364 messages=[
1365 {
1366 "role": "user",
1367 "content": "Say this is a test",
1368 }
1369 ],
1370 model="gpt-3.5-turbo",
1371 ),
1372 cast_to=httpx.Response,
1373 options={"headers": {"X-Stainless-Streamed-Raw-Response": "true"}},
1374 )
1375
1376 assert _get_open_connections(self.client) == 0
1377
1378 @pytest.mark.respx(base_url=base_url)
1379 @pytest.mark.asyncio
1380 async def test_status_error_within_httpx(self, respx_mock: MockRouter) -> None:
1381 respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1382
1383 def on_response(response: httpx.Response) -> None:
1384 raise httpx.HTTPStatusError(
1385 "Simulating an error inside httpx",
1386 response=response,
1387 request=response.request,
1388 )
1389
1390 client = AsyncOpenAI(
1391 base_url=base_url,
1392 api_key=api_key,
1393 _strict_response_validation=True,
1394 http_client=httpx.AsyncClient(
1395 event_hooks={
1396 "response": [on_response],
1397 }
1398 ),
1399 max_retries=0,
1400 )
1401 with pytest.raises(APIStatusError):
1402 await client.post("/foo", cast_to=httpx.Response)
1403