openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.3.9

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

1396lines · modeblame

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