openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.40.4

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

1615lines · modeblame

5cfb125aStainless Bot2 years ago1# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
08b8179aDavid Schnurr2 years ago2
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
30194f19stainless-app[bot]1 years ago20from openai._types import Omit
08b8179aDavid Schnurr2 years ago21from openai._models import BaseModel, FinalRequestOptions
86379b44Stainless Bot2 years ago22from openai._constants import RAW_RESPONSE_HEADER
08b8179aDavid Schnurr2 years ago23from openai._streaming import Stream, AsyncStream
a47375b7Stainless Bot2 years ago24from openai._exceptions import OpenAIError, APIStatusError, APITimeoutError, APIResponseValidationError
25from openai._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options
08b8179aDavid Schnurr2 years ago26
0733934fStainless Bot2 years ago27from .utils import update_env
28
08b8179aDavid Schnurr2 years ago29base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
30api_key = "My API Key"
31
32
33def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]:
34request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
35url = httpx.URL(request.url)
36return dict(url.params)
37
38
ba4f7a97Stainless Bot2 years ago39def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
40return 0.1
7aad3405Stainless Bot2 years ago41
42
43def _get_open_connections(client: OpenAI | AsyncOpenAI) -> int:
44transport = client._client._transport
45assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
46
47pool = transport._pool
48return len(pool._requests)
49
50
08b8179aDavid Schnurr2 years ago51class TestOpenAI:
52client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
53
54@pytest.mark.respx(base_url=base_url)
55def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago56respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago57
58response = self.client.post("/foo", cast_to=httpx.Response)
59assert response.status_code == 200
60assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago61assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago62
63@pytest.mark.respx(base_url=base_url)
64def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
65respx_mock.post("/foo").mock(
66return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
67)
68
69response = self.client.post("/foo", cast_to=httpx.Response)
70assert response.status_code == 200
71assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago72assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago73
74def test_copy(self) -> None:
75copied = self.client.copy()
76assert id(copied) != id(self.client)
77
78copied = self.client.copy(api_key="another My API Key")
79assert copied.api_key == "another My API Key"
80assert self.client.api_key == "My API Key"
81
82def test_copy_default_options(self) -> None:
83# options that have a default are overridden correctly
84copied = self.client.copy(max_retries=7)
85assert copied.max_retries == 7
86assert self.client.max_retries == 2
87
88copied2 = copied.copy(max_retries=6)
89assert copied2.max_retries == 6
90assert copied.max_retries == 7
91
92# timeout
93assert isinstance(self.client.timeout, httpx.Timeout)
94copied = self.client.copy(timeout=None)
95assert copied.timeout is None
96assert isinstance(self.client.timeout, httpx.Timeout)
97
98def test_copy_default_headers(self) -> None:
99client = OpenAI(
100base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
101)
102assert client.default_headers["X-Foo"] == "bar"
103
104# does not override the already given value when not specified
105copied = client.copy()
106assert copied.default_headers["X-Foo"] == "bar"
107
108# merges already given headers
109copied = client.copy(default_headers={"X-Bar": "stainless"})
110assert copied.default_headers["X-Foo"] == "bar"
111assert copied.default_headers["X-Bar"] == "stainless"
112
113# uses new values for any already given headers
114copied = client.copy(default_headers={"X-Foo": "stainless"})
115assert copied.default_headers["X-Foo"] == "stainless"
116
117# set_default_headers
118
119# completely overrides already set values
120copied = client.copy(set_default_headers={})
121assert copied.default_headers.get("X-Foo") is None
122
123copied = client.copy(set_default_headers={"X-Bar": "Robert"})
124assert copied.default_headers["X-Bar"] == "Robert"
125
126with pytest.raises(
127ValueError,
128match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
129):
130client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
131
132def test_copy_default_query(self) -> None:
133client = OpenAI(
134base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
135)
136assert _get_params(client)["foo"] == "bar"
137
138# does not override the already given value when not specified
139copied = client.copy()
140assert _get_params(copied)["foo"] == "bar"
141
142# merges already given params
143copied = client.copy(default_query={"bar": "stainless"})
144params = _get_params(copied)
145assert params["foo"] == "bar"
146assert params["bar"] == "stainless"
147
148# uses new values for any already given headers
149copied = client.copy(default_query={"foo": "stainless"})
150assert _get_params(copied)["foo"] == "stainless"
151
152# set_default_query
153
154# completely overrides already set values
155copied = client.copy(set_default_query={})
156assert _get_params(copied) == {}
157
158copied = client.copy(set_default_query={"bar": "Robert"})
159assert _get_params(copied)["bar"] == "Robert"
160
161with pytest.raises(
162ValueError,
163# TODO: update
164match="`default_query` and `set_default_query` arguments are mutually exclusive",
165):
166client.copy(set_default_query={}, default_query={"foo": "Bar"})
167
168def test_copy_signature(self) -> None:
169# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
170init_signature = inspect.signature(
171# mypy doesn't like that we access the `__init__` property.
172self.client.__init__, # type: ignore[misc]
173)
174copy_signature = inspect.signature(self.client.copy)
175exclude_params = {"transport", "proxies", "_strict_response_validation"}
176
177for name in init_signature.parameters.keys():
178if name in exclude_params:
179continue
180
181copy_param = copy_signature.parameters.get(name)
182assert copy_param is not None, f"copy() signature is missing the {name} param"
183
d052708aStainless Bot2 years ago184def test_copy_build_request(self) -> None:
185options = FinalRequestOptions(method="get", url="/foo")
186
187def build_request(options: FinalRequestOptions) -> None:
188client = self.client.copy()
189client._build_request(options)
190
191# ensure that the machinery is warmed up before tracing starts.
192build_request(options)
193gc.collect()
194
195tracemalloc.start(1000)
196
197snapshot_before = tracemalloc.take_snapshot()
198
199ITERATIONS = 10
200for _ in range(ITERATIONS):
201build_request(options)
202
ce04ec28Stainless Bot2 years ago203gc.collect()
d052708aStainless Bot2 years ago204snapshot_after = tracemalloc.take_snapshot()
205
206tracemalloc.stop()
207
208def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
209if diff.count == 0:
210# Avoid false positives by considering only leaks (i.e. allocations that persist).
211return
212
213if diff.count % ITERATIONS != 0:
214# Avoid false positives by considering only leaks that appear per iteration.
215return
216
217for frame in diff.traceback:
218if any(
219frame.filename.endswith(fragment)
220for fragment in [
221# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
222#
223# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago224"openai/_legacy_response.py",
d052708aStainless Bot2 years ago225"openai/_response.py",
226# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
227"openai/_compat.py",
228# Standard library leaks we don't care about.
229"/logging/__init__.py",
230]
231):
232return
233
234leaks.append(diff)
235
236leaks: list[tracemalloc.StatisticDiff] = []
237for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
238add_leak(leaks, diff)
239if leaks:
240for leak in leaks:
241print("MEMORY LEAK:", leak)
242for frame in leak.traceback:
243print(frame)
244raise AssertionError()
245
08b8179aDavid Schnurr2 years ago246def test_request_timeout(self) -> None:
247request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
248timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
249assert timeout == DEFAULT_TIMEOUT
250
251request = self.client._build_request(
252FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
253)
254timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
255assert timeout == httpx.Timeout(100.0)
256
257def test_client_timeout_option(self) -> None:
258client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0))
259
260request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
261timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
262assert timeout == httpx.Timeout(0)
263
264def test_http_client_timeout_option(self) -> None:
265# custom timeout given to the httpx client should be used
266with httpx.Client(timeout=None) as http_client:
267client = OpenAI(
268base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
269)
270
271request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
272timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
273assert timeout == httpx.Timeout(None)
274
275# no timeout given to the httpx client should not use the httpx default
276with httpx.Client() as http_client:
277client = OpenAI(
278base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
279)
280
281request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
282timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
283assert timeout == DEFAULT_TIMEOUT
284
285# explicitly passing the default timeout currently results in it being ignored
286with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
287client = OpenAI(
288base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
289)
290
291request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
292timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
293assert timeout == DEFAULT_TIMEOUT # our default
294
dae0ec80Stainless Bot2 years ago295async def test_invalid_http_client(self) -> None:
296with pytest.raises(TypeError, match="Invalid `http_client` arg"):
297async with httpx.AsyncClient() as http_client:
298OpenAI(
299base_url=base_url,
300api_key=api_key,
301_strict_response_validation=True,
302http_client=cast(Any, http_client),
303)
304
08b8179aDavid Schnurr2 years ago305def test_default_headers_option(self) -> None:
306client = OpenAI(
307base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
308)
309request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
310assert request.headers.get("x-foo") == "bar"
311assert request.headers.get("x-stainless-lang") == "python"
312
313client2 = OpenAI(
314base_url=base_url,
315api_key=api_key,
316_strict_response_validation=True,
317default_headers={
318"X-Foo": "stainless",
319"X-Stainless-Lang": "my-overriding-header",
320},
321)
322request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
323assert request.headers.get("x-foo") == "stainless"
324assert request.headers.get("x-stainless-lang") == "my-overriding-header"
325
326def test_validate_headers(self) -> None:
327client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
328request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
329assert request.headers.get("Authorization") == f"Bearer {api_key}"
330
e967f5a5Stainless Bot2 years ago331with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago332with update_env(**{"OPENAI_API_KEY": Omit()}):
333client2 = OpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago334_ = client2
335
336def test_default_query_option(self) -> None:
337client = OpenAI(
338base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
339)
340request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
341url = httpx.URL(request.url)
342assert dict(url.params) == {"query_param": "bar"}
343
344request = client._build_request(
345FinalRequestOptions(
346method="get",
347url="/foo",
348params={"foo": "baz", "query_param": "overriden"},
349)
350)
351url = httpx.URL(request.url)
352assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
353
354def test_request_extra_json(self) -> None:
355request = self.client._build_request(
356FinalRequestOptions(
357method="post",
358url="/foo",
359json_data={"foo": "bar"},
360extra_json={"baz": False},
361),
362)
363data = json.loads(request.content.decode("utf-8"))
364assert data == {"foo": "bar", "baz": False}
365
366request = self.client._build_request(
367FinalRequestOptions(
368method="post",
369url="/foo",
370extra_json={"baz": False},
371),
372)
373data = json.loads(request.content.decode("utf-8"))
374assert data == {"baz": False}
375
376# `extra_json` takes priority over `json_data` when keys clash
377request = self.client._build_request(
378FinalRequestOptions(
379method="post",
380url="/foo",
381json_data={"foo": "bar", "baz": True},
382extra_json={"baz": None},
383),
384)
385data = json.loads(request.content.decode("utf-8"))
386assert data == {"foo": "bar", "baz": None}
387
388def test_request_extra_headers(self) -> None:
389request = self.client._build_request(
390FinalRequestOptions(
391method="post",
392url="/foo",
393**make_request_options(extra_headers={"X-Foo": "Foo"}),
394),
395)
396assert request.headers.get("X-Foo") == "Foo"
397
398# `extra_headers` takes priority over `default_headers` when keys clash
399request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
400FinalRequestOptions(
401method="post",
402url="/foo",
403**make_request_options(
404extra_headers={"X-Bar": "false"},
405),
406),
407)
408assert request.headers.get("X-Bar") == "false"
409
410def test_request_extra_query(self) -> None:
411request = self.client._build_request(
412FinalRequestOptions(
413method="post",
414url="/foo",
415**make_request_options(
416extra_query={"my_query_param": "Foo"},
417),
418),
419)
31573844Stainless Bot2 years ago420params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago421assert params == {"my_query_param": "Foo"}
422
423# if both `query` and `extra_query` are given, they are merged
424request = self.client._build_request(
425FinalRequestOptions(
426method="post",
427url="/foo",
428**make_request_options(
429query={"bar": "1"},
430extra_query={"foo": "2"},
431),
432),
433)
31573844Stainless Bot2 years ago434params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago435assert params == {"bar": "1", "foo": "2"}
436
437# `extra_query` takes priority over `query` when keys clash
438request = self.client._build_request(
439FinalRequestOptions(
440method="post",
441url="/foo",
442**make_request_options(
443query={"foo": "1"},
444extra_query={"foo": "2"},
445),
446),
447)
31573844Stainless Bot2 years ago448params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago449assert params == {"foo": "2"}
450
22713fd0Stainless Bot2 years ago451def test_multipart_repeating_array(self, client: OpenAI) -> None:
452request = client._build_request(
453FinalRequestOptions.construct(
454method="get",
455url="/foo",
456headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
457json_data={"array": ["foo", "bar"]},
458files=[("foo.txt", b"hello world")],
459)
460)
461
462assert request.read().split(b"\r\n") == [
463b"--6b7ba517decee4a450543ea6ae821c82",
464b'Content-Disposition: form-data; name="array[]"',
465b"",
466b"foo",
467b"--6b7ba517decee4a450543ea6ae821c82",
468b'Content-Disposition: form-data; name="array[]"',
469b"",
470b"bar",
471b"--6b7ba517decee4a450543ea6ae821c82",
472b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
473b"Content-Type: application/octet-stream",
474b"",
475b"hello world",
476b"--6b7ba517decee4a450543ea6ae821c82--",
477b"",
478]
479
08b8179aDavid Schnurr2 years ago480@pytest.mark.respx(base_url=base_url)
481def test_basic_union_response(self, respx_mock: MockRouter) -> None:
482class Model1(BaseModel):
483name: str
484
485class Model2(BaseModel):
486foo: str
487
488respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
489
490response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
491assert isinstance(response, Model2)
492assert response.foo == "bar"
493
494@pytest.mark.respx(base_url=base_url)
495def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
496"""Union of objects with the same field name using a different type"""
497
498class Model1(BaseModel):
499foo: int
500
501class Model2(BaseModel):
502foo: str
503
504respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
505
506response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
507assert isinstance(response, Model2)
508assert response.foo == "bar"
509
510respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
511
512response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
513assert isinstance(response, Model1)
514assert response.foo == 1
515
c26014e2Stainless Bot2 years ago516@pytest.mark.respx(base_url=base_url)
517def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
518"""
519Response that sets Content-Type to something other than application/json but returns json data
520"""
521
522class Model(BaseModel):
523foo: int
524
525respx_mock.get("/foo").mock(
526return_value=httpx.Response(
527200,
528content=json.dumps({"foo": 2}),
529headers={"Content-Type": "application/text"},
530)
531)
532
533response = self.client.get("/foo", cast_to=Model)
534assert isinstance(response, Model)
535assert response.foo == 2
536
f6f38a9bStainless Bot2 years ago537def test_base_url_setter(self) -> None:
538client = OpenAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
539assert client.base_url == "https://example.com/from_init/"
540
541client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
542
543assert client.base_url == "https://example.com/from_setter/"
544
0733934fStainless Bot2 years ago545def test_base_url_env(self) -> None:
546with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
547client = OpenAI(api_key=api_key, _strict_response_validation=True)
548assert client.base_url == "http://localhost:5000/from/env/"
549
08b8179aDavid Schnurr2 years ago550@pytest.mark.parametrize(
551"client",
552[
553OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
554OpenAI(
555base_url="http://localhost:5000/custom/path/",
556api_key=api_key,
557_strict_response_validation=True,
558http_client=httpx.Client(),
559),
560],
561ids=["standard", "custom http client"],
562)
563def test_base_url_trailing_slash(self, client: OpenAI) -> None:
564request = client._build_request(
565FinalRequestOptions(
566method="post",
567url="/foo",
568json_data={"foo": "bar"},
569),
570)
571assert request.url == "http://localhost:5000/custom/path/foo"
572
573@pytest.mark.parametrize(
574"client",
575[
576OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
577OpenAI(
578base_url="http://localhost:5000/custom/path/",
579api_key=api_key,
580_strict_response_validation=True,
581http_client=httpx.Client(),
582),
583],
584ids=["standard", "custom http client"],
585)
586def test_base_url_no_trailing_slash(self, client: OpenAI) -> None:
587request = client._build_request(
588FinalRequestOptions(
589method="post",
590url="/foo",
591json_data={"foo": "bar"},
592),
593)
594assert request.url == "http://localhost:5000/custom/path/foo"
595
596@pytest.mark.parametrize(
597"client",
598[
599OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
600OpenAI(
601base_url="http://localhost:5000/custom/path/",
602api_key=api_key,
603_strict_response_validation=True,
604http_client=httpx.Client(),
605),
606],
607ids=["standard", "custom http client"],
608)
609def test_absolute_request_url(self, client: OpenAI) -> None:
610request = client._build_request(
611FinalRequestOptions(
612method="post",
613url="https://myapi.com/foo",
614json_data={"foo": "bar"},
615),
616)
617assert request.url == "https://myapi.com/foo"
618
619def test_copied_client_does_not_close_http(self) -> None:
620client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
621assert not client.is_closed()
622
623copied = client.copy()
624assert copied is not client
625
a7ebc260Stainless Bot2 years ago626del copied
08b8179aDavid Schnurr2 years ago627
628assert not client.is_closed()
629
630def test_client_context_manager(self) -> None:
631client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
632with client as c2:
633assert c2 is client
634assert not c2.is_closed()
635assert not client.is_closed()
636assert client.is_closed()
637
638@pytest.mark.respx(base_url=base_url)
639def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
640class Model(BaseModel):
641foo: str
642
643respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
644
645with pytest.raises(APIResponseValidationError) as exc:
646self.client.get("/foo", cast_to=Model)
647
648assert isinstance(exc.value.__cause__, ValidationError)
649
07079085Stainless Bot2 years ago650def test_client_max_retries_validation(self) -> None:
651with pytest.raises(TypeError, match=r"max_retries cannot be None"):
652OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None))
653
08b8179aDavid Schnurr2 years ago654@pytest.mark.respx(base_url=base_url)
655def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
656class Model(BaseModel):
657name: str
658
659respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
660
86379b44Stainless Bot2 years ago661stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
662assert isinstance(stream, Stream)
663stream.response.close()
08b8179aDavid Schnurr2 years ago664
665@pytest.mark.respx(base_url=base_url)
666def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
667class Model(BaseModel):
668name: str
669
670respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
671
672strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
673
674with pytest.raises(APIResponseValidationError):
675strict_client.get("/foo", cast_to=Model)
676
677client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
678
679response = client.get("/foo", cast_to=Model)
680assert isinstance(response, str) # type: ignore[unreachable]
681
682@pytest.mark.parametrize(
683"remaining_retries,retry_after,timeout",
684[
685[3, "20", 20],
686[3, "0", 0.5],
687[3, "-10", 0.5],
688[3, "60", 60],
689[3, "61", 0.5],
690[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
691[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
692[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
693[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
694[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
695[3, "99999999999999999999999999999999999", 0.5],
696[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
697[3, "", 0.5],
698[2, "", 0.5 * 2.0],
699[1, "", 0.5 * 4.0],
700],
701)
702@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
703def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
704client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
705
706headers = httpx.Headers({"retry-after": retry_after})
707options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
708calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
709assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
710
ba4f7a97Stainless Bot2 years ago711@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
712@pytest.mark.respx(base_url=base_url)
713def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
714respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
715
716with pytest.raises(APITimeoutError):
717self.client.post(
718"/chat/completions",
76382e3cStainless Bot2 years ago719body=cast(
720object,
721dict(
722messages=[
723{
724"role": "user",
725"content": "Say this is a test",
726}
727],
728model="gpt-3.5-turbo",
729),
ba4f7a97Stainless Bot2 years ago730),
731cast_to=httpx.Response,
86379b44Stainless Bot2 years ago732options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
ba4f7a97Stainless Bot2 years ago733)
7aad3405Stainless Bot2 years ago734
735assert _get_open_connections(self.client) == 0
736
ba4f7a97Stainless Bot2 years ago737@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago738@pytest.mark.respx(base_url=base_url)
ba4f7a97Stainless Bot2 years ago739def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
740respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago741
ba4f7a97Stainless Bot2 years ago742with pytest.raises(APIStatusError):
743self.client.post(
744"/chat/completions",
76382e3cStainless Bot2 years ago745body=cast(
746object,
747dict(
748messages=[
749{
750"role": "user",
751"content": "Say this is a test",
752}
753],
754model="gpt-3.5-turbo",
755),
ba4f7a97Stainless Bot2 years ago756),
757cast_to=httpx.Response,
86379b44Stainless Bot2 years ago758options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
7aad3405Stainless Bot2 years ago759)
760
ba4f7a97Stainless Bot2 years ago761assert _get_open_connections(self.client) == 0
7aad3405Stainless Bot2 years ago762
98d8b2acstainless-app[bot]1 years ago763@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
764@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
765@pytest.mark.respx(base_url=base_url)
766def test_retries_taken(self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter) -> None:
767client = client.with_options(max_retries=4)
768
769nb_retries = 0
770
771def retry_handler(_request: httpx.Request) -> httpx.Response:
772nonlocal nb_retries
773if nb_retries < failures_before_success:
774nb_retries += 1
775return httpx.Response(500)
776return httpx.Response(200)
777
778respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
779
780response = client.chat.completions.with_raw_response.create(
781messages=[
782{
bf1ca86cRobert Craigie1 years ago783"content": "string",
98d8b2acstainless-app[bot]1 years ago784"role": "system",
785}
786],
bf1ca86cRobert Craigie1 years ago787model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago788)
789
790assert response.retries_taken == failures_before_success
791
792@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
793@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
794@pytest.mark.respx(base_url=base_url)
795def test_retries_taken_new_response_class(
796self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
797) -> None:
798client = client.with_options(max_retries=4)
799
800nb_retries = 0
801
802def retry_handler(_request: httpx.Request) -> httpx.Response:
803nonlocal nb_retries
804if nb_retries < failures_before_success:
805nb_retries += 1
806return httpx.Response(500)
807return httpx.Response(200)
808
809respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
810
811with client.chat.completions.with_streaming_response.create(
812messages=[
813{
bf1ca86cRobert Craigie1 years ago814"content": "string",
98d8b2acstainless-app[bot]1 years ago815"role": "system",
816}
817],
bf1ca86cRobert Craigie1 years ago818model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago819) as response:
820assert response.retries_taken == failures_before_success
821
08b8179aDavid Schnurr2 years ago822
823class TestAsyncOpenAI:
824client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
825
826@pytest.mark.respx(base_url=base_url)
827@pytest.mark.asyncio
828async def test_raw_response(self, respx_mock: MockRouter) -> None:
c5975bd0Stainless Bot2 years ago829respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago830
831response = await self.client.post("/foo", cast_to=httpx.Response)
832assert response.status_code == 200
833assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago834assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago835
836@pytest.mark.respx(base_url=base_url)
837@pytest.mark.asyncio
838async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
839respx_mock.post("/foo").mock(
840return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
841)
842
843response = await self.client.post("/foo", cast_to=httpx.Response)
844assert response.status_code == 200
845assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago846assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago847
848def test_copy(self) -> None:
849copied = self.client.copy()
850assert id(copied) != id(self.client)
851
852copied = self.client.copy(api_key="another My API Key")
853assert copied.api_key == "another My API Key"
854assert self.client.api_key == "My API Key"
855
856def test_copy_default_options(self) -> None:
857# options that have a default are overridden correctly
858copied = self.client.copy(max_retries=7)
859assert copied.max_retries == 7
860assert self.client.max_retries == 2
861
862copied2 = copied.copy(max_retries=6)
863assert copied2.max_retries == 6
864assert copied.max_retries == 7
865
866# timeout
867assert isinstance(self.client.timeout, httpx.Timeout)
868copied = self.client.copy(timeout=None)
869assert copied.timeout is None
870assert isinstance(self.client.timeout, httpx.Timeout)
871
872def test_copy_default_headers(self) -> None:
873client = AsyncOpenAI(
874base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
875)
876assert client.default_headers["X-Foo"] == "bar"
877
878# does not override the already given value when not specified
879copied = client.copy()
880assert copied.default_headers["X-Foo"] == "bar"
881
882# merges already given headers
883copied = client.copy(default_headers={"X-Bar": "stainless"})
884assert copied.default_headers["X-Foo"] == "bar"
885assert copied.default_headers["X-Bar"] == "stainless"
886
887# uses new values for any already given headers
888copied = client.copy(default_headers={"X-Foo": "stainless"})
889assert copied.default_headers["X-Foo"] == "stainless"
890
891# set_default_headers
892
893# completely overrides already set values
894copied = client.copy(set_default_headers={})
895assert copied.default_headers.get("X-Foo") is None
896
897copied = client.copy(set_default_headers={"X-Bar": "Robert"})
898assert copied.default_headers["X-Bar"] == "Robert"
899
900with pytest.raises(
901ValueError,
902match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
903):
904client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
905
906def test_copy_default_query(self) -> None:
907client = AsyncOpenAI(
908base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
909)
910assert _get_params(client)["foo"] == "bar"
911
912# does not override the already given value when not specified
913copied = client.copy()
914assert _get_params(copied)["foo"] == "bar"
915
916# merges already given params
917copied = client.copy(default_query={"bar": "stainless"})
918params = _get_params(copied)
919assert params["foo"] == "bar"
920assert params["bar"] == "stainless"
921
922# uses new values for any already given headers
923copied = client.copy(default_query={"foo": "stainless"})
924assert _get_params(copied)["foo"] == "stainless"
925
926# set_default_query
927
928# completely overrides already set values
929copied = client.copy(set_default_query={})
930assert _get_params(copied) == {}
931
932copied = client.copy(set_default_query={"bar": "Robert"})
933assert _get_params(copied)["bar"] == "Robert"
934
935with pytest.raises(
936ValueError,
937# TODO: update
938match="`default_query` and `set_default_query` arguments are mutually exclusive",
939):
940client.copy(set_default_query={}, default_query={"foo": "Bar"})
941
942def test_copy_signature(self) -> None:
943# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
944init_signature = inspect.signature(
945# mypy doesn't like that we access the `__init__` property.
946self.client.__init__, # type: ignore[misc]
947)
948copy_signature = inspect.signature(self.client.copy)
949exclude_params = {"transport", "proxies", "_strict_response_validation"}
950
951for name in init_signature.parameters.keys():
952if name in exclude_params:
953continue
954
955copy_param = copy_signature.parameters.get(name)
956assert copy_param is not None, f"copy() signature is missing the {name} param"
957
d052708aStainless Bot2 years ago958def test_copy_build_request(self) -> None:
959options = FinalRequestOptions(method="get", url="/foo")
960
961def build_request(options: FinalRequestOptions) -> None:
962client = self.client.copy()
963client._build_request(options)
964
965# ensure that the machinery is warmed up before tracing starts.
966build_request(options)
967gc.collect()
968
969tracemalloc.start(1000)
970
971snapshot_before = tracemalloc.take_snapshot()
972
973ITERATIONS = 10
974for _ in range(ITERATIONS):
975build_request(options)
976
ce04ec28Stainless Bot2 years ago977gc.collect()
d052708aStainless Bot2 years ago978snapshot_after = tracemalloc.take_snapshot()
979
980tracemalloc.stop()
981
982def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
983if diff.count == 0:
984# Avoid false positives by considering only leaks (i.e. allocations that persist).
985return
986
987if diff.count % ITERATIONS != 0:
988# Avoid false positives by considering only leaks that appear per iteration.
989return
990
991for frame in diff.traceback:
992if any(
993frame.filename.endswith(fragment)
994for fragment in [
995# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
996#
997# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago998"openai/_legacy_response.py",
d052708aStainless Bot2 years ago999"openai/_response.py",
1000# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
1001"openai/_compat.py",
1002# Standard library leaks we don't care about.
1003"/logging/__init__.py",
1004]
1005):
1006return
1007
1008leaks.append(diff)
1009
1010leaks: list[tracemalloc.StatisticDiff] = []
1011for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
1012add_leak(leaks, diff)
1013if leaks:
1014for leak in leaks:
1015print("MEMORY LEAK:", leak)
1016for frame in leak.traceback:
1017print(frame)
1018raise AssertionError()
1019
08b8179aDavid Schnurr2 years ago1020async def test_request_timeout(self) -> None:
1021request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
1022timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1023assert timeout == DEFAULT_TIMEOUT
1024
1025request = self.client._build_request(
1026FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
1027)
1028timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1029assert timeout == httpx.Timeout(100.0)
1030
1031async def test_client_timeout_option(self) -> None:
1032client = AsyncOpenAI(
1033base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
1034)
1035
1036request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1037timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1038assert timeout == httpx.Timeout(0)
1039
1040async def test_http_client_timeout_option(self) -> None:
1041# custom timeout given to the httpx client should be used
1042async with httpx.AsyncClient(timeout=None) as http_client:
1043client = AsyncOpenAI(
1044base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1045)
1046
1047request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1048timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1049assert timeout == httpx.Timeout(None)
1050
1051# no timeout given to the httpx client should not use the httpx default
1052async with httpx.AsyncClient() as http_client:
1053client = AsyncOpenAI(
1054base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1055)
1056
1057request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1058timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1059assert timeout == DEFAULT_TIMEOUT
1060
1061# explicitly passing the default timeout currently results in it being ignored
1062async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
1063client = AsyncOpenAI(
1064base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1065)
1066
1067request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1068timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1069assert timeout == DEFAULT_TIMEOUT # our default
1070
dae0ec80Stainless Bot2 years ago1071def test_invalid_http_client(self) -> None:
1072with pytest.raises(TypeError, match="Invalid `http_client` arg"):
1073with httpx.Client() as http_client:
1074AsyncOpenAI(
1075base_url=base_url,
1076api_key=api_key,
1077_strict_response_validation=True,
1078http_client=cast(Any, http_client),
1079)
1080
08b8179aDavid Schnurr2 years ago1081def test_default_headers_option(self) -> None:
1082client = AsyncOpenAI(
1083base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1084)
1085request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1086assert request.headers.get("x-foo") == "bar"
1087assert request.headers.get("x-stainless-lang") == "python"
1088
1089client2 = AsyncOpenAI(
1090base_url=base_url,
1091api_key=api_key,
1092_strict_response_validation=True,
1093default_headers={
1094"X-Foo": "stainless",
1095"X-Stainless-Lang": "my-overriding-header",
1096},
1097)
1098request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
1099assert request.headers.get("x-foo") == "stainless"
1100assert request.headers.get("x-stainless-lang") == "my-overriding-header"
1101
1102def test_validate_headers(self) -> None:
1103client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1104request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1105assert request.headers.get("Authorization") == f"Bearer {api_key}"
1106
e967f5a5Stainless Bot2 years ago1107with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago1108with update_env(**{"OPENAI_API_KEY": Omit()}):
1109client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago1110_ = client2
1111
1112def test_default_query_option(self) -> None:
1113client = AsyncOpenAI(
1114base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
1115)
1116request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1117url = httpx.URL(request.url)
1118assert dict(url.params) == {"query_param": "bar"}
1119
1120request = client._build_request(
1121FinalRequestOptions(
1122method="get",
1123url="/foo",
1124params={"foo": "baz", "query_param": "overriden"},
1125)
1126)
1127url = httpx.URL(request.url)
1128assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
1129
1130def test_request_extra_json(self) -> None:
1131request = self.client._build_request(
1132FinalRequestOptions(
1133method="post",
1134url="/foo",
1135json_data={"foo": "bar"},
1136extra_json={"baz": False},
1137),
1138)
1139data = json.loads(request.content.decode("utf-8"))
1140assert data == {"foo": "bar", "baz": False}
1141
1142request = self.client._build_request(
1143FinalRequestOptions(
1144method="post",
1145url="/foo",
1146extra_json={"baz": False},
1147),
1148)
1149data = json.loads(request.content.decode("utf-8"))
1150assert data == {"baz": False}
1151
1152# `extra_json` takes priority over `json_data` when keys clash
1153request = self.client._build_request(
1154FinalRequestOptions(
1155method="post",
1156url="/foo",
1157json_data={"foo": "bar", "baz": True},
1158extra_json={"baz": None},
1159),
1160)
1161data = json.loads(request.content.decode("utf-8"))
1162assert data == {"foo": "bar", "baz": None}
1163
1164def test_request_extra_headers(self) -> None:
1165request = self.client._build_request(
1166FinalRequestOptions(
1167method="post",
1168url="/foo",
1169**make_request_options(extra_headers={"X-Foo": "Foo"}),
1170),
1171)
1172assert request.headers.get("X-Foo") == "Foo"
1173
1174# `extra_headers` takes priority over `default_headers` when keys clash
1175request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
1176FinalRequestOptions(
1177method="post",
1178url="/foo",
1179**make_request_options(
1180extra_headers={"X-Bar": "false"},
1181),
1182),
1183)
1184assert request.headers.get("X-Bar") == "false"
1185
1186def test_request_extra_query(self) -> None:
1187request = self.client._build_request(
1188FinalRequestOptions(
1189method="post",
1190url="/foo",
1191**make_request_options(
1192extra_query={"my_query_param": "Foo"},
1193),
1194),
1195)
31573844Stainless Bot2 years ago1196params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1197assert params == {"my_query_param": "Foo"}
1198
1199# if both `query` and `extra_query` are given, they are merged
1200request = self.client._build_request(
1201FinalRequestOptions(
1202method="post",
1203url="/foo",
1204**make_request_options(
1205query={"bar": "1"},
1206extra_query={"foo": "2"},
1207),
1208),
1209)
31573844Stainless Bot2 years ago1210params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1211assert params == {"bar": "1", "foo": "2"}
1212
1213# `extra_query` takes priority over `query` when keys clash
1214request = self.client._build_request(
1215FinalRequestOptions(
1216method="post",
1217url="/foo",
1218**make_request_options(
1219query={"foo": "1"},
1220extra_query={"foo": "2"},
1221),
1222),
1223)
31573844Stainless Bot2 years ago1224params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1225assert params == {"foo": "2"}
1226
22713fd0Stainless Bot2 years ago1227def test_multipart_repeating_array(self, async_client: AsyncOpenAI) -> None:
1228request = async_client._build_request(
1229FinalRequestOptions.construct(
1230method="get",
1231url="/foo",
1232headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
1233json_data={"array": ["foo", "bar"]},
1234files=[("foo.txt", b"hello world")],
1235)
1236)
1237
1238assert request.read().split(b"\r\n") == [
1239b"--6b7ba517decee4a450543ea6ae821c82",
1240b'Content-Disposition: form-data; name="array[]"',
1241b"",
1242b"foo",
1243b"--6b7ba517decee4a450543ea6ae821c82",
1244b'Content-Disposition: form-data; name="array[]"',
1245b"",
1246b"bar",
1247b"--6b7ba517decee4a450543ea6ae821c82",
1248b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
1249b"Content-Type: application/octet-stream",
1250b"",
1251b"hello world",
1252b"--6b7ba517decee4a450543ea6ae821c82--",
1253b"",
1254]
1255
08b8179aDavid Schnurr2 years ago1256@pytest.mark.respx(base_url=base_url)
1257async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
1258class Model1(BaseModel):
1259name: str
1260
1261class Model2(BaseModel):
1262foo: str
1263
1264respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1265
1266response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1267assert isinstance(response, Model2)
1268assert response.foo == "bar"
1269
1270@pytest.mark.respx(base_url=base_url)
1271async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
1272"""Union of objects with the same field name using a different type"""
1273
1274class Model1(BaseModel):
1275foo: int
1276
1277class Model2(BaseModel):
1278foo: str
1279
1280respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1281
1282response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1283assert isinstance(response, Model2)
1284assert response.foo == "bar"
1285
1286respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1287
1288response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
1289assert isinstance(response, Model1)
1290assert response.foo == 1
1291
c26014e2Stainless Bot2 years ago1292@pytest.mark.respx(base_url=base_url)
1293async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
1294"""
1295Response that sets Content-Type to something other than application/json but returns json data
1296"""
1297
1298class Model(BaseModel):
1299foo: int
1300
1301respx_mock.get("/foo").mock(
1302return_value=httpx.Response(
1303200,
1304content=json.dumps({"foo": 2}),
1305headers={"Content-Type": "application/text"},
1306)
1307)
1308
1309response = await self.client.get("/foo", cast_to=Model)
1310assert isinstance(response, Model)
1311assert response.foo == 2
1312
f6f38a9bStainless Bot2 years ago1313def test_base_url_setter(self) -> None:
1314client = AsyncOpenAI(
1315base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1316)
1317assert client.base_url == "https://example.com/from_init/"
1318
1319client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1320
1321assert client.base_url == "https://example.com/from_setter/"
1322
0733934fStainless Bot2 years ago1323def test_base_url_env(self) -> None:
1324with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1325client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1326assert client.base_url == "http://localhost:5000/from/env/"
1327
08b8179aDavid Schnurr2 years ago1328@pytest.mark.parametrize(
1329"client",
1330[
1331AsyncOpenAI(
1332base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1333),
1334AsyncOpenAI(
1335base_url="http://localhost:5000/custom/path/",
1336api_key=api_key,
1337_strict_response_validation=True,
1338http_client=httpx.AsyncClient(),
1339),
1340],
1341ids=["standard", "custom http client"],
1342)
1343def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
1344request = client._build_request(
1345FinalRequestOptions(
1346method="post",
1347url="/foo",
1348json_data={"foo": "bar"},
1349),
1350)
1351assert request.url == "http://localhost:5000/custom/path/foo"
1352
1353@pytest.mark.parametrize(
1354"client",
1355[
1356AsyncOpenAI(
1357base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1358),
1359AsyncOpenAI(
1360base_url="http://localhost:5000/custom/path/",
1361api_key=api_key,
1362_strict_response_validation=True,
1363http_client=httpx.AsyncClient(),
1364),
1365],
1366ids=["standard", "custom http client"],
1367)
1368def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
1369request = client._build_request(
1370FinalRequestOptions(
1371method="post",
1372url="/foo",
1373json_data={"foo": "bar"},
1374),
1375)
1376assert request.url == "http://localhost:5000/custom/path/foo"
1377
1378@pytest.mark.parametrize(
1379"client",
1380[
1381AsyncOpenAI(
1382base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1383),
1384AsyncOpenAI(
1385base_url="http://localhost:5000/custom/path/",
1386api_key=api_key,
1387_strict_response_validation=True,
1388http_client=httpx.AsyncClient(),
1389),
1390],
1391ids=["standard", "custom http client"],
1392)
1393def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
1394request = client._build_request(
1395FinalRequestOptions(
1396method="post",
1397url="https://myapi.com/foo",
1398json_data={"foo": "bar"},
1399),
1400)
1401assert request.url == "https://myapi.com/foo"
1402
1403async def test_copied_client_does_not_close_http(self) -> None:
1404client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1405assert not client.is_closed()
1406
1407copied = client.copy()
1408assert copied is not client
1409
a7ebc260Stainless Bot2 years ago1410del copied
08b8179aDavid Schnurr2 years ago1411
1412await asyncio.sleep(0.2)
1413assert not client.is_closed()
1414
1415async def test_client_context_manager(self) -> None:
1416client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1417async with client as c2:
1418assert c2 is client
1419assert not c2.is_closed()
1420assert not client.is_closed()
1421assert client.is_closed()
1422
1423@pytest.mark.respx(base_url=base_url)
1424@pytest.mark.asyncio
1425async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
1426class Model(BaseModel):
1427foo: str
1428
1429respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1430
1431with pytest.raises(APIResponseValidationError) as exc:
1432await self.client.get("/foo", cast_to=Model)
1433
1434assert isinstance(exc.value.__cause__, ValidationError)
1435
07079085Stainless Bot2 years ago1436async def test_client_max_retries_validation(self) -> None:
1437with pytest.raises(TypeError, match=r"max_retries cannot be None"):
1438AsyncOpenAI(
1439base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)
1440)
1441
08b8179aDavid Schnurr2 years ago1442@pytest.mark.respx(base_url=base_url)
1443@pytest.mark.asyncio
1444async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
1445class Model(BaseModel):
1446name: str
1447
1448respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1449
86379b44Stainless Bot2 years ago1450stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
1451assert isinstance(stream, AsyncStream)
1452await stream.response.aclose()
08b8179aDavid Schnurr2 years ago1453
1454@pytest.mark.respx(base_url=base_url)
1455@pytest.mark.asyncio
1456async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1457class Model(BaseModel):
1458name: str
1459
1460respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1461
1462strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1463
1464with pytest.raises(APIResponseValidationError):
1465await strict_client.get("/foo", cast_to=Model)
1466
1467client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
1468
1469response = await client.get("/foo", cast_to=Model)
1470assert isinstance(response, str) # type: ignore[unreachable]
1471
1472@pytest.mark.parametrize(
1473"remaining_retries,retry_after,timeout",
1474[
1475[3, "20", 20],
1476[3, "0", 0.5],
1477[3, "-10", 0.5],
1478[3, "60", 60],
1479[3, "61", 0.5],
1480[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1481[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1482[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1483[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1484[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1485[3, "99999999999999999999999999999999999", 0.5],
1486[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1487[3, "", 0.5],
1488[2, "", 0.5 * 2.0],
1489[1, "", 0.5 * 4.0],
1490],
1491)
1492@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
1493@pytest.mark.asyncio
1494async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
1495client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1496
1497headers = httpx.Headers({"retry-after": retry_after})
1498options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
1499calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
1500assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
7aad3405Stainless Bot2 years ago1501
ba4f7a97Stainless Bot2 years ago1502@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1503@pytest.mark.respx(base_url=base_url)
1504async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
1505respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
1506
1507with pytest.raises(APITimeoutError):
1508await self.client.post(
1509"/chat/completions",
76382e3cStainless Bot2 years ago1510body=cast(
1511object,
1512dict(
1513messages=[
1514{
1515"role": "user",
1516"content": "Say this is a test",
1517}
1518],
1519model="gpt-3.5-turbo",
1520),
ba4f7a97Stainless Bot2 years ago1521),
1522cast_to=httpx.Response,
86379b44Stainless Bot2 years ago1523options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
ba4f7a97Stainless Bot2 years ago1524)
7aad3405Stainless Bot2 years ago1525
1526assert _get_open_connections(self.client) == 0
1527
ba4f7a97Stainless Bot2 years ago1528@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago1529@pytest.mark.respx(base_url=base_url)
ba4f7a97Stainless Bot2 years ago1530async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
1531respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago1532
ba4f7a97Stainless Bot2 years ago1533with pytest.raises(APIStatusError):
1534await self.client.post(
1535"/chat/completions",
76382e3cStainless Bot2 years ago1536body=cast(
1537object,
1538dict(
1539messages=[
1540{
1541"role": "user",
1542"content": "Say this is a test",
1543}
1544],
1545model="gpt-3.5-turbo",
1546),
ba4f7a97Stainless Bot2 years ago1547),
1548cast_to=httpx.Response,
86379b44Stainless Bot2 years ago1549options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
7aad3405Stainless Bot2 years ago1550)
1551
ba4f7a97Stainless Bot2 years ago1552assert _get_open_connections(self.client) == 0
98d8b2acstainless-app[bot]1 years ago1553
1554@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1555@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1556@pytest.mark.respx(base_url=base_url)
1557@pytest.mark.asyncio
1558async def test_retries_taken(
1559self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1560) -> None:
1561client = async_client.with_options(max_retries=4)
1562
1563nb_retries = 0
1564
1565def retry_handler(_request: httpx.Request) -> httpx.Response:
1566nonlocal nb_retries
1567if nb_retries < failures_before_success:
1568nb_retries += 1
1569return httpx.Response(500)
1570return httpx.Response(200)
1571
1572respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1573
1574response = await client.chat.completions.with_raw_response.create(
1575messages=[
1576{
bf1ca86cRobert Craigie1 years ago1577"content": "string",
98d8b2acstainless-app[bot]1 years ago1578"role": "system",
1579}
1580],
bf1ca86cRobert Craigie1 years ago1581model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1582)
1583
1584assert response.retries_taken == failures_before_success
1585
1586@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1587@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1588@pytest.mark.respx(base_url=base_url)
1589@pytest.mark.asyncio
1590async def test_retries_taken_new_response_class(
1591self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1592) -> None:
1593client = async_client.with_options(max_retries=4)
1594
1595nb_retries = 0
1596
1597def retry_handler(_request: httpx.Request) -> httpx.Response:
1598nonlocal nb_retries
1599if nb_retries < failures_before_success:
1600nb_retries += 1
1601return httpx.Response(500)
1602return httpx.Response(200)
1603
1604respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1605
1606async with client.chat.completions.with_streaming_response.create(
1607messages=[
1608{
bf1ca86cRobert Craigie1 years ago1609"content": "string",
98d8b2acstainless-app[bot]1 years ago1610"role": "system",
1611}
1612],
bf1ca86cRobert Craigie1 years ago1613model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1614) as response:
1615assert response.retries_taken == failures_before_success