openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v2.25.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

2209lines · 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
d8901d28Seth Gilchrist1 years ago7import sys
08b8179aDavid Schnurr2 years ago8import json
9import asyncio
10import inspect
a532f6efstainless-app[bot]5 months ago11import dataclasses
d052708aStainless Bot2 years ago12import tracemalloc
3d93d66dstainless-app[bot]5 months ago13from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Protocol, Coroutine, cast
08b8179aDavid Schnurr2 years ago14from unittest import mock
a532f6efstainless-app[bot]5 months ago15from typing_extensions import Literal, AsyncIterator, override
08b8179aDavid Schnurr2 years ago16
17import httpx
18import pytest
19from respx import MockRouter
20from pydantic import ValidationError
21
22from openai import OpenAI, AsyncOpenAI, APIResponseValidationError
30194f19stainless-app[bot]1 years ago23from openai._types import Omit
4756247cstainless-app[bot]9 months ago24from openai._utils import asyncify
08b8179aDavid Schnurr2 years ago25from openai._models import BaseModel, FinalRequestOptions
26from openai._streaming import Stream, AsyncStream
a47375b7Stainless Bot2 years ago27from openai._exceptions import OpenAIError, APIStatusError, APITimeoutError, APIResponseValidationError
cc2c1fc1stainless-app[bot]1 years ago28from openai._base_client import (
29DEFAULT_TIMEOUT,
30HTTPX_DEFAULT_TIMEOUT,
31BaseClient,
4756247cstainless-app[bot]9 months ago32OtherPlatform,
cc2c1fc1stainless-app[bot]1 years ago33DefaultHttpxClient,
34DefaultAsyncHttpxClient,
4756247cstainless-app[bot]9 months ago35get_platform,
cc2c1fc1stainless-app[bot]1 years ago36make_request_options,
37)
08b8179aDavid Schnurr2 years ago38
0733934fStainless Bot2 years ago39from .utils import update_env
40
a532f6efstainless-app[bot]5 months ago41T = TypeVar("T")
08b8179aDavid Schnurr2 years ago42base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
43api_key = "My API Key"
44
45
25d16be1Johan Stenberg (MSFT)9 months ago46class MockRequestCall(Protocol):
47request: httpx.Request
48
49
08b8179aDavid Schnurr2 years ago50def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]:
51request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
52url = httpx.URL(request.url)
53return dict(url.params)
54
55
ba4f7a97Stainless Bot2 years ago56def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
57return 0.1
7aad3405Stainless Bot2 years ago58
59
a532f6efstainless-app[bot]5 months ago60def mirror_request_content(request: httpx.Request) -> httpx.Response:
61return httpx.Response(200, content=request.content)
62
63
64# note: we can't use the httpx.MockTransport class as it consumes the request
65# body itself, which means we can't test that the body is read lazily
66class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport):
67def __init__(
68self,
69handler: Callable[[httpx.Request], httpx.Response]
70| Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]],
71) -> None:
72self.handler = handler
73
74@override
75def handle_request(
76self,
77request: httpx.Request,
78) -> httpx.Response:
79assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function"
80assert inspect.isfunction(self.handler), "handler must be a function"
81return self.handler(request)
82
83@override
84async def handle_async_request(
85self,
86request: httpx.Request,
87) -> httpx.Response:
88assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function"
89return await self.handler(request)
90
91
92@dataclasses.dataclass
93class Counter:
94value: int = 0
95
96
97def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]:
98for item in iterable:
99if counter:
100counter.value += 1
101yield item
102
103
104async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]:
105for item in iterable:
106if counter:
107counter.value += 1
108yield item
109
110
7aad3405Stainless Bot2 years ago111def _get_open_connections(client: OpenAI | AsyncOpenAI) -> int:
112transport = client._client._transport
113assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
114
115pool = transport._pool
116return len(pool._requests)
117
118
08b8179aDavid Schnurr2 years ago119class TestOpenAI:
120@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago121def test_raw_response(self, respx_mock: MockRouter, client: OpenAI) -> None:
c5975bd0Stainless Bot2 years ago122respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago123
53278443stainless-app[bot]7 months ago124response = client.post("/foo", cast_to=httpx.Response)
08b8179aDavid Schnurr2 years ago125assert response.status_code == 200
126assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago127assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago128
129@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago130def test_raw_response_for_binary(self, respx_mock: MockRouter, client: OpenAI) -> None:
08b8179aDavid Schnurr2 years ago131respx_mock.post("/foo").mock(
132return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
133)
134
53278443stainless-app[bot]7 months ago135response = client.post("/foo", cast_to=httpx.Response)
08b8179aDavid Schnurr2 years ago136assert response.status_code == 200
137assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago138assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago139
53278443stainless-app[bot]7 months ago140def test_copy(self, client: OpenAI) -> None:
141copied = client.copy()
142assert id(copied) != id(client)
08b8179aDavid Schnurr2 years ago143
53278443stainless-app[bot]7 months ago144copied = client.copy(api_key="another My API Key")
08b8179aDavid Schnurr2 years ago145assert copied.api_key == "another My API Key"
53278443stainless-app[bot]7 months ago146assert client.api_key == "My API Key"
08b8179aDavid Schnurr2 years ago147
53278443stainless-app[bot]7 months ago148def test_copy_default_options(self, client: OpenAI) -> None:
08b8179aDavid Schnurr2 years ago149# options that have a default are overridden correctly
53278443stainless-app[bot]7 months ago150copied = client.copy(max_retries=7)
08b8179aDavid Schnurr2 years ago151assert copied.max_retries == 7
53278443stainless-app[bot]7 months ago152assert client.max_retries == 2
08b8179aDavid Schnurr2 years ago153
154copied2 = copied.copy(max_retries=6)
155assert copied2.max_retries == 6
156assert copied.max_retries == 7
157
158# timeout
53278443stainless-app[bot]7 months ago159assert isinstance(client.timeout, httpx.Timeout)
160copied = client.copy(timeout=None)
08b8179aDavid Schnurr2 years ago161assert copied.timeout is None
53278443stainless-app[bot]7 months ago162assert isinstance(client.timeout, httpx.Timeout)
08b8179aDavid Schnurr2 years ago163
164def test_copy_default_headers(self) -> None:
165client = OpenAI(
166base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
167)
168assert client.default_headers["X-Foo"] == "bar"
169
170# does not override the already given value when not specified
171copied = client.copy()
172assert copied.default_headers["X-Foo"] == "bar"
173
174# merges already given headers
175copied = client.copy(default_headers={"X-Bar": "stainless"})
176assert copied.default_headers["X-Foo"] == "bar"
177assert copied.default_headers["X-Bar"] == "stainless"
178
179# uses new values for any already given headers
180copied = client.copy(default_headers={"X-Foo": "stainless"})
181assert copied.default_headers["X-Foo"] == "stainless"
182
183# set_default_headers
184
185# completely overrides already set values
186copied = client.copy(set_default_headers={})
187assert copied.default_headers.get("X-Foo") is None
188
189copied = client.copy(set_default_headers={"X-Bar": "Robert"})
190assert copied.default_headers["X-Bar"] == "Robert"
191
192with pytest.raises(
193ValueError,
194match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
195):
196client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
53278443stainless-app[bot]7 months ago197client.close()
08b8179aDavid Schnurr2 years ago198
199def test_copy_default_query(self) -> None:
200client = OpenAI(
201base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
202)
203assert _get_params(client)["foo"] == "bar"
204
205# does not override the already given value when not specified
206copied = client.copy()
207assert _get_params(copied)["foo"] == "bar"
208
209# merges already given params
210copied = client.copy(default_query={"bar": "stainless"})
211params = _get_params(copied)
212assert params["foo"] == "bar"
213assert params["bar"] == "stainless"
214
215# uses new values for any already given headers
216copied = client.copy(default_query={"foo": "stainless"})
217assert _get_params(copied)["foo"] == "stainless"
218
219# set_default_query
220
221# completely overrides already set values
222copied = client.copy(set_default_query={})
223assert _get_params(copied) == {}
224
225copied = client.copy(set_default_query={"bar": "Robert"})
226assert _get_params(copied)["bar"] == "Robert"
227
228with pytest.raises(
229ValueError,
230# TODO: update
231match="`default_query` and `set_default_query` arguments are mutually exclusive",
232):
233client.copy(set_default_query={}, default_query={"foo": "Bar"})
234
53278443stainless-app[bot]7 months ago235client.close()
236
237def test_copy_signature(self, client: OpenAI) -> None:
08b8179aDavid Schnurr2 years ago238# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
239init_signature = inspect.signature(
240# mypy doesn't like that we access the `__init__` property.
53278443stainless-app[bot]7 months ago241client.__init__, # type: ignore[misc]
08b8179aDavid Schnurr2 years ago242)
53278443stainless-app[bot]7 months ago243copy_signature = inspect.signature(client.copy)
08b8179aDavid Schnurr2 years ago244exclude_params = {"transport", "proxies", "_strict_response_validation"}
245
246for name in init_signature.parameters.keys():
247if name in exclude_params:
248continue
249
250copy_param = copy_signature.parameters.get(name)
251assert copy_param is not None, f"copy() signature is missing the {name} param"
252
18e0b36astainless-app[bot]1 years ago253@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
53278443stainless-app[bot]7 months ago254def test_copy_build_request(self, client: OpenAI) -> None:
d052708aStainless Bot2 years ago255options = FinalRequestOptions(method="get", url="/foo")
256
257def build_request(options: FinalRequestOptions) -> None:
53278443stainless-app[bot]7 months ago258client_copy = client.copy()
259client_copy._build_request(options)
d052708aStainless Bot2 years ago260
261# ensure that the machinery is warmed up before tracing starts.
262build_request(options)
263gc.collect()
264
265tracemalloc.start(1000)
266
267snapshot_before = tracemalloc.take_snapshot()
268
269ITERATIONS = 10
270for _ in range(ITERATIONS):
271build_request(options)
272
ce04ec28Stainless Bot2 years ago273gc.collect()
d052708aStainless Bot2 years ago274snapshot_after = tracemalloc.take_snapshot()
275
276tracemalloc.stop()
277
278def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
279if diff.count == 0:
280# Avoid false positives by considering only leaks (i.e. allocations that persist).
281return
282
283if diff.count % ITERATIONS != 0:
284# Avoid false positives by considering only leaks that appear per iteration.
285return
286
287for frame in diff.traceback:
288if any(
289frame.filename.endswith(fragment)
290for fragment in [
291# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
292#
293# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago294"openai/_legacy_response.py",
d052708aStainless Bot2 years ago295"openai/_response.py",
296# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
297"openai/_compat.py",
298# Standard library leaks we don't care about.
299"/logging/__init__.py",
300]
301):
302return
303
304leaks.append(diff)
305
306leaks: list[tracemalloc.StatisticDiff] = []
307for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
308add_leak(leaks, diff)
309if leaks:
310for leak in leaks:
311print("MEMORY LEAK:", leak)
312for frame in leak.traceback:
313print(frame)
314raise AssertionError()
315
53278443stainless-app[bot]7 months ago316def test_request_timeout(self, client: OpenAI) -> None:
317request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago318timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
319assert timeout == DEFAULT_TIMEOUT
320
53278443stainless-app[bot]7 months ago321request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)))
08b8179aDavid Schnurr2 years ago322timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
323assert timeout == httpx.Timeout(100.0)
324
325def test_client_timeout_option(self) -> None:
326client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0))
327
328request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
329timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
330assert timeout == httpx.Timeout(0)
331
53278443stainless-app[bot]7 months ago332client.close()
333
08b8179aDavid Schnurr2 years ago334def test_http_client_timeout_option(self) -> None:
335# custom timeout given to the httpx client should be used
336with httpx.Client(timeout=None) as http_client:
337client = OpenAI(
338base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
339)
340
341request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
342timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
343assert timeout == httpx.Timeout(None)
344
53278443stainless-app[bot]7 months ago345client.close()
346
08b8179aDavid Schnurr2 years ago347# no timeout given to the httpx client should not use the httpx default
348with httpx.Client() as http_client:
349client = OpenAI(
350base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
351)
352
353request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
354timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
355assert timeout == DEFAULT_TIMEOUT
356
53278443stainless-app[bot]7 months ago357client.close()
358
08b8179aDavid Schnurr2 years ago359# explicitly passing the default timeout currently results in it being ignored
360with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
361client = OpenAI(
362base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
363)
364
365request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
366timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
367assert timeout == DEFAULT_TIMEOUT # our default
368
53278443stainless-app[bot]7 months ago369client.close()
370
dae0ec80Stainless Bot2 years ago371async def test_invalid_http_client(self) -> None:
372with pytest.raises(TypeError, match="Invalid `http_client` arg"):
373async with httpx.AsyncClient() as http_client:
374OpenAI(
375base_url=base_url,
376api_key=api_key,
377_strict_response_validation=True,
378http_client=cast(Any, http_client),
379)
380
08b8179aDavid Schnurr2 years ago381def test_default_headers_option(self) -> None:
53278443stainless-app[bot]7 months ago382test_client = OpenAI(
08b8179aDavid Schnurr2 years ago383base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
384)
53278443stainless-app[bot]7 months ago385request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago386assert request.headers.get("x-foo") == "bar"
387assert request.headers.get("x-stainless-lang") == "python"
388
53278443stainless-app[bot]7 months ago389test_client2 = OpenAI(
08b8179aDavid Schnurr2 years ago390base_url=base_url,
391api_key=api_key,
392_strict_response_validation=True,
393default_headers={
394"X-Foo": "stainless",
395"X-Stainless-Lang": "my-overriding-header",
396},
397)
53278443stainless-app[bot]7 months ago398request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago399assert request.headers.get("x-foo") == "stainless"
400assert request.headers.get("x-stainless-lang") == "my-overriding-header"
401
53278443stainless-app[bot]7 months ago402test_client.close()
403test_client2.close()
404
08b8179aDavid Schnurr2 years ago405def test_validate_headers(self) -> None:
406client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
25d16be1Johan Stenberg (MSFT)9 months ago407options = client._prepare_options(FinalRequestOptions(method="get", url="/foo"))
408request = client._build_request(options)
409
08b8179aDavid Schnurr2 years ago410assert request.headers.get("Authorization") == f"Bearer {api_key}"
411
e967f5a5Stainless Bot2 years ago412with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago413with update_env(**{"OPENAI_API_KEY": Omit()}):
414client2 = OpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago415_ = client2
416
417def test_default_query_option(self) -> None:
418client = OpenAI(
419base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
420)
421request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
422url = httpx.URL(request.url)
423assert dict(url.params) == {"query_param": "bar"}
424
425request = client._build_request(
426FinalRequestOptions(
427method="get",
428url="/foo",
eba67815stainless-app[bot]1 years ago429params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago430)
431)
432url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago433assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago434
53278443stainless-app[bot]7 months ago435client.close()
436
437def test_request_extra_json(self, client: OpenAI) -> None:
438request = client._build_request(
08b8179aDavid Schnurr2 years ago439FinalRequestOptions(
440method="post",
441url="/foo",
442json_data={"foo": "bar"},
443extra_json={"baz": False},
444),
445)
446data = json.loads(request.content.decode("utf-8"))
447assert data == {"foo": "bar", "baz": False}
448
53278443stainless-app[bot]7 months ago449request = client._build_request(
08b8179aDavid Schnurr2 years ago450FinalRequestOptions(
451method="post",
452url="/foo",
453extra_json={"baz": False},
454),
455)
456data = json.loads(request.content.decode("utf-8"))
457assert data == {"baz": False}
458
459# `extra_json` takes priority over `json_data` when keys clash
53278443stainless-app[bot]7 months ago460request = client._build_request(
08b8179aDavid Schnurr2 years ago461FinalRequestOptions(
462method="post",
463url="/foo",
464json_data={"foo": "bar", "baz": True},
465extra_json={"baz": None},
466),
467)
468data = json.loads(request.content.decode("utf-8"))
469assert data == {"foo": "bar", "baz": None}
470
53278443stainless-app[bot]7 months ago471def test_request_extra_headers(self, client: OpenAI) -> None:
472request = client._build_request(
08b8179aDavid Schnurr2 years ago473FinalRequestOptions(
474method="post",
475url="/foo",
476**make_request_options(extra_headers={"X-Foo": "Foo"}),
477),
478)
479assert request.headers.get("X-Foo") == "Foo"
480
481# `extra_headers` takes priority over `default_headers` when keys clash
53278443stainless-app[bot]7 months ago482request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
08b8179aDavid Schnurr2 years ago483FinalRequestOptions(
484method="post",
485url="/foo",
486**make_request_options(
487extra_headers={"X-Bar": "false"},
488),
489),
490)
491assert request.headers.get("X-Bar") == "false"
492
53278443stainless-app[bot]7 months ago493def test_request_extra_query(self, client: OpenAI) -> None:
494request = client._build_request(
08b8179aDavid Schnurr2 years ago495FinalRequestOptions(
496method="post",
497url="/foo",
498**make_request_options(
499extra_query={"my_query_param": "Foo"},
500),
501),
502)
31573844Stainless Bot2 years ago503params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago504assert params == {"my_query_param": "Foo"}
505
506# if both `query` and `extra_query` are given, they are merged
53278443stainless-app[bot]7 months ago507request = client._build_request(
08b8179aDavid Schnurr2 years ago508FinalRequestOptions(
509method="post",
510url="/foo",
511**make_request_options(
512query={"bar": "1"},
513extra_query={"foo": "2"},
514),
515),
516)
31573844Stainless Bot2 years ago517params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago518assert params == {"bar": "1", "foo": "2"}
519
520# `extra_query` takes priority over `query` when keys clash
53278443stainless-app[bot]7 months ago521request = client._build_request(
08b8179aDavid Schnurr2 years ago522FinalRequestOptions(
523method="post",
524url="/foo",
525**make_request_options(
526query={"foo": "1"},
527extra_query={"foo": "2"},
528),
529),
530)
31573844Stainless Bot2 years ago531params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago532assert params == {"foo": "2"}
533
22713fd0Stainless Bot2 years ago534def test_multipart_repeating_array(self, client: OpenAI) -> None:
535request = client._build_request(
536FinalRequestOptions.construct(
fcbb5983stainless-app[bot]11 months ago537method="post",
22713fd0Stainless Bot2 years ago538url="/foo",
539headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
540json_data={"array": ["foo", "bar"]},
541files=[("foo.txt", b"hello world")],
542)
543)
544
545assert request.read().split(b"\r\n") == [
546b"--6b7ba517decee4a450543ea6ae821c82",
547b'Content-Disposition: form-data; name="array[]"',
548b"",
549b"foo",
550b"--6b7ba517decee4a450543ea6ae821c82",
551b'Content-Disposition: form-data; name="array[]"',
552b"",
553b"bar",
554b"--6b7ba517decee4a450543ea6ae821c82",
555b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
556b"Content-Type: application/octet-stream",
557b"",
558b"hello world",
559b"--6b7ba517decee4a450543ea6ae821c82--",
560b"",
561]
562
a532f6efstainless-app[bot]5 months ago563@pytest.mark.respx(base_url=base_url)
564def test_binary_content_upload(self, respx_mock: MockRouter, client: OpenAI) -> None:
565respx_mock.post("/upload").mock(side_effect=mirror_request_content)
566
567file_content = b"Hello, this is a test file."
568
569response = client.post(
570"/upload",
571content=file_content,
572cast_to=httpx.Response,
573options={"headers": {"Content-Type": "application/octet-stream"}},
574)
575
576assert response.status_code == 200
577assert response.request.headers["Content-Type"] == "application/octet-stream"
578assert response.content == file_content
579
580def test_binary_content_upload_with_iterator(self) -> None:
581file_content = b"Hello, this is a test file."
582counter = Counter()
583iterator = _make_sync_iterator([file_content], counter=counter)
584
585def mock_handler(request: httpx.Request) -> httpx.Response:
586assert counter.value == 0, "the request body should not have been read"
587return httpx.Response(200, content=request.read())
588
589with OpenAI(
590base_url=base_url,
591api_key=api_key,
592_strict_response_validation=True,
593http_client=httpx.Client(transport=MockTransport(handler=mock_handler)),
594) as client:
595response = client.post(
596"/upload",
597content=iterator,
598cast_to=httpx.Response,
599options={"headers": {"Content-Type": "application/octet-stream"}},
600)
601
602assert response.status_code == 200
603assert response.request.headers["Content-Type"] == "application/octet-stream"
604assert response.content == file_content
605assert counter.value == 1
606
607@pytest.mark.respx(base_url=base_url)
608def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: OpenAI) -> None:
609respx_mock.post("/upload").mock(side_effect=mirror_request_content)
610
611file_content = b"Hello, this is a test file."
612
613with pytest.deprecated_call(
614match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
615):
616response = client.post(
617"/upload",
618body=file_content,
619cast_to=httpx.Response,
620options={"headers": {"Content-Type": "application/octet-stream"}},
621)
622
623assert response.status_code == 200
624assert response.request.headers["Content-Type"] == "application/octet-stream"
625assert response.content == file_content
626
08b8179aDavid Schnurr2 years ago627@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago628def test_basic_union_response(self, respx_mock: MockRouter, client: OpenAI) -> None:
08b8179aDavid Schnurr2 years ago629class Model1(BaseModel):
630name: str
631
632class Model2(BaseModel):
633foo: str
634
635respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
636
53278443stainless-app[bot]7 months ago637response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago638assert isinstance(response, Model2)
639assert response.foo == "bar"
640
641@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago642def test_union_response_different_types(self, respx_mock: MockRouter, client: OpenAI) -> None:
08b8179aDavid Schnurr2 years ago643"""Union of objects with the same field name using a different type"""
644
645class Model1(BaseModel):
646foo: int
647
648class Model2(BaseModel):
649foo: str
650
651respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
652
53278443stainless-app[bot]7 months ago653response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago654assert isinstance(response, Model2)
655assert response.foo == "bar"
656
657respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
658
53278443stainless-app[bot]7 months ago659response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago660assert isinstance(response, Model1)
661assert response.foo == 1
662
c26014e2Stainless Bot2 years ago663@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago664def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: OpenAI) -> None:
c26014e2Stainless Bot2 years ago665"""
666Response that sets Content-Type to something other than application/json but returns json data
667"""
668
669class Model(BaseModel):
670foo: int
671
672respx_mock.get("/foo").mock(
673return_value=httpx.Response(
674200,
675content=json.dumps({"foo": 2}),
676headers={"Content-Type": "application/text"},
677)
678)
679
53278443stainless-app[bot]7 months ago680response = client.get("/foo", cast_to=Model)
c26014e2Stainless Bot2 years ago681assert isinstance(response, Model)
682assert response.foo == 2
683
f6f38a9bStainless Bot2 years ago684def test_base_url_setter(self) -> None:
685client = OpenAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
686assert client.base_url == "https://example.com/from_init/"
687
688client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
689
690assert client.base_url == "https://example.com/from_setter/"
691
53278443stainless-app[bot]7 months ago692client.close()
693
0733934fStainless Bot2 years ago694def test_base_url_env(self) -> None:
695with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
696client = OpenAI(api_key=api_key, _strict_response_validation=True)
697assert client.base_url == "http://localhost:5000/from/env/"
698
08b8179aDavid Schnurr2 years ago699@pytest.mark.parametrize(
700"client",
701[
702OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
703OpenAI(
704base_url="http://localhost:5000/custom/path/",
705api_key=api_key,
706_strict_response_validation=True,
707http_client=httpx.Client(),
708),
709],
710ids=["standard", "custom http client"],
711)
712def test_base_url_trailing_slash(self, client: OpenAI) -> None:
713request = client._build_request(
714FinalRequestOptions(
715method="post",
716url="/foo",
717json_data={"foo": "bar"},
718),
719)
720assert request.url == "http://localhost:5000/custom/path/foo"
53278443stainless-app[bot]7 months ago721client.close()
08b8179aDavid Schnurr2 years ago722
723@pytest.mark.parametrize(
724"client",
725[
726OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
727OpenAI(
728base_url="http://localhost:5000/custom/path/",
729api_key=api_key,
730_strict_response_validation=True,
731http_client=httpx.Client(),
732),
733],
734ids=["standard", "custom http client"],
735)
736def test_base_url_no_trailing_slash(self, client: OpenAI) -> None:
737request = client._build_request(
738FinalRequestOptions(
739method="post",
740url="/foo",
741json_data={"foo": "bar"},
742),
743)
744assert request.url == "http://localhost:5000/custom/path/foo"
53278443stainless-app[bot]7 months ago745client.close()
08b8179aDavid Schnurr2 years ago746
747@pytest.mark.parametrize(
748"client",
749[
750OpenAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True),
751OpenAI(
752base_url="http://localhost:5000/custom/path/",
753api_key=api_key,
754_strict_response_validation=True,
755http_client=httpx.Client(),
756),
757],
758ids=["standard", "custom http client"],
759)
760def test_absolute_request_url(self, client: OpenAI) -> None:
761request = client._build_request(
762FinalRequestOptions(
763method="post",
764url="https://myapi.com/foo",
765json_data={"foo": "bar"},
766),
767)
768assert request.url == "https://myapi.com/foo"
53278443stainless-app[bot]7 months ago769client.close()
08b8179aDavid Schnurr2 years ago770
771def test_copied_client_does_not_close_http(self) -> None:
53278443stainless-app[bot]7 months ago772test_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
773assert not test_client.is_closed()
08b8179aDavid Schnurr2 years ago774
53278443stainless-app[bot]7 months ago775copied = test_client.copy()
776assert copied is not test_client
08b8179aDavid Schnurr2 years ago777
a7ebc260Stainless Bot2 years ago778del copied
08b8179aDavid Schnurr2 years ago779
53278443stainless-app[bot]7 months ago780assert not test_client.is_closed()
08b8179aDavid Schnurr2 years ago781
782def test_client_context_manager(self) -> None:
53278443stainless-app[bot]7 months ago783test_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
784with test_client as c2:
785assert c2 is test_client
08b8179aDavid Schnurr2 years ago786assert not c2.is_closed()
53278443stainless-app[bot]7 months ago787assert not test_client.is_closed()
788assert test_client.is_closed()
08b8179aDavid Schnurr2 years ago789
790@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago791def test_client_response_validation_error(self, respx_mock: MockRouter, client: OpenAI) -> None:
08b8179aDavid Schnurr2 years ago792class Model(BaseModel):
793foo: str
794
795respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
796
797with pytest.raises(APIResponseValidationError) as exc:
53278443stainless-app[bot]7 months ago798client.get("/foo", cast_to=Model)
08b8179aDavid Schnurr2 years ago799
800assert isinstance(exc.value.__cause__, ValidationError)
801
07079085Stainless Bot2 years ago802def test_client_max_retries_validation(self) -> None:
803with pytest.raises(TypeError, match=r"max_retries cannot be None"):
804OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None))
805
08b8179aDavid Schnurr2 years ago806@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago807def test_default_stream_cls(self, respx_mock: MockRouter, client: OpenAI) -> None:
08b8179aDavid Schnurr2 years ago808class Model(BaseModel):
809name: str
810
811respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
812
53278443stainless-app[bot]7 months ago813stream = client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
86379b44Stainless Bot2 years ago814assert isinstance(stream, Stream)
815stream.response.close()
08b8179aDavid Schnurr2 years ago816
817@pytest.mark.respx(base_url=base_url)
818def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
819class Model(BaseModel):
820name: str
821
822respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
823
824strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
825
826with pytest.raises(APIResponseValidationError):
827strict_client.get("/foo", cast_to=Model)
828
53278443stainless-app[bot]7 months ago829non_strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
08b8179aDavid Schnurr2 years ago830
53278443stainless-app[bot]7 months ago831response = non_strict_client.get("/foo", cast_to=Model)
08b8179aDavid Schnurr2 years ago832assert isinstance(response, str) # type: ignore[unreachable]
833
53278443stainless-app[bot]7 months ago834strict_client.close()
835non_strict_client.close()
836
08b8179aDavid Schnurr2 years ago837@pytest.mark.parametrize(
838"remaining_retries,retry_after,timeout",
839[
840[3, "20", 20],
841[3, "0", 0.5],
842[3, "-10", 0.5],
843[3, "60", 60],
844[3, "61", 0.5],
845[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
846[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
847[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
848[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
849[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
850[3, "99999999999999999999999999999999999", 0.5],
851[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
852[3, "", 0.5],
853[2, "", 0.5 * 2.0],
854[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago855[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago856],
857)
858@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
53278443stainless-app[bot]7 months ago859def test_parse_retry_after_header(
860self, remaining_retries: int, retry_after: str, timeout: float, client: OpenAI
861) -> None:
08b8179aDavid Schnurr2 years ago862headers = httpx.Headers({"retry-after": retry_after})
863options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
864calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
865assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
866
ba4f7a97Stainless Bot2 years ago867@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
868@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago869def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: OpenAI) -> None:
ba4f7a97Stainless Bot2 years ago870respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
871
872with pytest.raises(APITimeoutError):
0bef1d02stainless-app[bot]1 years ago873client.chat.completions.with_streaming_response.create(
874messages=[
875{
876"content": "string",
877"role": "developer",
878}
879],
9b1bb6eestainless-app[bot]3 months ago880model="gpt-5.4",
0bef1d02stainless-app[bot]1 years ago881).__enter__()
7aad3405Stainless Bot2 years ago882
53278443stainless-app[bot]7 months ago883assert _get_open_connections(client) == 0
7aad3405Stainless Bot2 years ago884
ba4f7a97Stainless Bot2 years ago885@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago886@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago887def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: OpenAI) -> None:
ba4f7a97Stainless Bot2 years ago888respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago889
ba4f7a97Stainless Bot2 years ago890with pytest.raises(APIStatusError):
0bef1d02stainless-app[bot]1 years ago891client.chat.completions.with_streaming_response.create(
892messages=[
893{
894"content": "string",
895"role": "developer",
896}
897],
9b1bb6eestainless-app[bot]3 months ago898model="gpt-5.4",
0bef1d02stainless-app[bot]1 years ago899).__enter__()
53278443stainless-app[bot]7 months ago900assert _get_open_connections(client) == 0
7aad3405Stainless Bot2 years ago901
98d8b2acstainless-app[bot]1 years ago902@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
903@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
904@pytest.mark.respx(base_url=base_url)
6634f525stainless-app[bot]1 years ago905@pytest.mark.parametrize("failure_mode", ["status", "exception"])
906def test_retries_taken(
907self,
908client: OpenAI,
909failures_before_success: int,
910failure_mode: Literal["status", "exception"],
911respx_mock: MockRouter,
912) -> None:
98d8b2acstainless-app[bot]1 years ago913client = client.with_options(max_retries=4)
914
915nb_retries = 0
916
917def retry_handler(_request: httpx.Request) -> httpx.Response:
918nonlocal nb_retries
919if nb_retries < failures_before_success:
920nb_retries += 1
6634f525stainless-app[bot]1 years ago921if failure_mode == "exception":
922raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago923return httpx.Response(500)
924return httpx.Response(200)
925
926respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
927
928response = client.chat.completions.with_raw_response.create(
929messages=[
930{
bf1ca86cRobert Craigie1 years ago931"content": "string",
575ff607stainless-app[bot]1 years ago932"role": "developer",
98d8b2acstainless-app[bot]1 years ago933}
934],
9b1bb6eestainless-app[bot]3 months ago935model="gpt-5.4",
98d8b2acstainless-app[bot]1 years ago936)
937
938assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago939assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
940
941@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
942@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
943@pytest.mark.respx(base_url=base_url)
944def test_omit_retry_count_header(
945self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
946) -> None:
947client = client.with_options(max_retries=4)
948
949nb_retries = 0
950
951def retry_handler(_request: httpx.Request) -> httpx.Response:
952nonlocal nb_retries
953if nb_retries < failures_before_success:
954nb_retries += 1
955return httpx.Response(500)
956return httpx.Response(200)
957
958respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
959
960response = client.chat.completions.with_raw_response.create(
961messages=[
962{
963"content": "string",
575ff607stainless-app[bot]1 years ago964"role": "developer",
5449e208Stainless Bot1 years ago965}
966],
9b1bb6eestainless-app[bot]3 months ago967model="gpt-5.4",
5449e208Stainless Bot1 years ago968extra_headers={"x-stainless-retry-count": Omit()},
969)
970
971assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
972
973@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
974@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
975@pytest.mark.respx(base_url=base_url)
976def test_overwrite_retry_count_header(
977self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
978) -> None:
979client = client.with_options(max_retries=4)
980
981nb_retries = 0
982
983def retry_handler(_request: httpx.Request) -> httpx.Response:
984nonlocal nb_retries
985if nb_retries < failures_before_success:
986nb_retries += 1
987return httpx.Response(500)
988return httpx.Response(200)
989
990respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
991
992response = client.chat.completions.with_raw_response.create(
993messages=[
994{
995"content": "string",
575ff607stainless-app[bot]1 years ago996"role": "developer",
5449e208Stainless Bot1 years ago997}
998],
9b1bb6eestainless-app[bot]3 months ago999model="gpt-5.4",
5449e208Stainless Bot1 years ago1000extra_headers={"x-stainless-retry-count": "42"},
1001)
1002
1003assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago1004
1005@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1006@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1007@pytest.mark.respx(base_url=base_url)
1008def test_retries_taken_new_response_class(
1009self, client: OpenAI, failures_before_success: int, respx_mock: MockRouter
1010) -> None:
1011client = client.with_options(max_retries=4)
1012
1013nb_retries = 0
1014
1015def retry_handler(_request: httpx.Request) -> httpx.Response:
1016nonlocal nb_retries
1017if nb_retries < failures_before_success:
1018nb_retries += 1
1019return httpx.Response(500)
1020return httpx.Response(200)
1021
1022respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1023
1024with client.chat.completions.with_streaming_response.create(
1025messages=[
1026{
bf1ca86cRobert Craigie1 years ago1027"content": "string",
575ff607stainless-app[bot]1 years ago1028"role": "developer",
98d8b2acstainless-app[bot]1 years ago1029}
1030],
9b1bb6eestainless-app[bot]3 months ago1031model="gpt-5.4",
98d8b2acstainless-app[bot]1 years ago1032) as response:
1033assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1034assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
98d8b2acstainless-app[bot]1 years ago1035
cc2c1fc1stainless-app[bot]1 years ago1036def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
1037# Test that the proxy environment variables are set correctly
1038monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
656e3cabstainless-app[bot]4 months ago1039# Delete in case our environment has any proxy env vars set
588d239cstainless-app[bot]4 months ago1040monkeypatch.delenv("HTTP_PROXY", raising=False)
656e3cabstainless-app[bot]4 months ago1041monkeypatch.delenv("ALL_PROXY", raising=False)
1042monkeypatch.delenv("NO_PROXY", raising=False)
1043monkeypatch.delenv("http_proxy", raising=False)
1044monkeypatch.delenv("https_proxy", raising=False)
1045monkeypatch.delenv("all_proxy", raising=False)
1046monkeypatch.delenv("no_proxy", raising=False)
cc2c1fc1stainless-app[bot]1 years ago1047
1048client = DefaultHttpxClient()
1049
1050mounts = tuple(client._mounts.items())
1051assert len(mounts) == 1
1052assert mounts[0][0].pattern == "https://"
1053
1054@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
1055def test_default_client_creation(self) -> None:
1056# Ensure that the client can be initialized without any exceptions
1057DefaultHttpxClient(
1058verify=True,
1059cert=None,
1060trust_env=True,
1061http1=True,
1062http2=False,
1063limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
1064)
1065
cca09707stainless-app[bot]1 years ago1066@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1067def test_follow_redirects(self, respx_mock: MockRouter, client: OpenAI) -> None:
cca09707stainless-app[bot]1 years ago1068# Test that the default follow_redirects=True allows following redirects
1069respx_mock.post("/redirect").mock(
1070return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1071)
1072respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1073
53278443stainless-app[bot]7 months ago1074response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
cca09707stainless-app[bot]1 years ago1075assert response.status_code == 200
1076assert response.json() == {"status": "ok"}
1077
1078@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1079def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: OpenAI) -> None:
cca09707stainless-app[bot]1 years ago1080# Test that follow_redirects=False prevents following redirects
1081respx_mock.post("/redirect").mock(
1082return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1083)
1084
1085with pytest.raises(APIStatusError) as exc_info:
53278443stainless-app[bot]7 months ago1086client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response)
cca09707stainless-app[bot]1 years ago1087
1088assert exc_info.value.response.status_code == 302
1089assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
1090
25d16be1Johan Stenberg (MSFT)9 months ago1091def test_api_key_before_after_refresh_provider(self) -> None:
1092client = OpenAI(base_url=base_url, api_key=lambda: "test_bearer_token")
1093
1094assert client.api_key == ""
1095assert "Authorization" not in client.auth_headers
1096
1097client._refresh_api_key()
1098
1099assert client.api_key == "test_bearer_token"
1100assert client.auth_headers.get("Authorization") == "Bearer test_bearer_token"
1101
1102def test_api_key_before_after_refresh_str(self) -> None:
1103client = OpenAI(base_url=base_url, api_key="test_api_key")
1104
1105assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
1106client._refresh_api_key()
1107
1108assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
1109
1110@pytest.mark.respx()
1111def test_api_key_refresh_on_retry(self, respx_mock: MockRouter) -> None:
1112respx_mock.post(base_url + "/chat/completions").mock(
1113side_effect=[
1114httpx.Response(500, json={"error": "server error"}),
1115httpx.Response(200, json={"foo": "bar"}),
1116]
1117)
1118
1119counter = 0
1120
1121def token_provider() -> str:
1122nonlocal counter
1123
1124counter += 1
1125
1126if counter == 1:
1127return "first"
1128
1129return "second"
1130
1131client = OpenAI(base_url=base_url, api_key=token_provider)
1132client.chat.completions.create(messages=[], model="gpt-4")
1133
1134calls = cast("list[MockRequestCall]", respx_mock.calls)
1135assert len(calls) == 2
1136
1137assert calls[0].request.headers.get("Authorization") == "Bearer first"
1138assert calls[1].request.headers.get("Authorization") == "Bearer second"
1139
1140def test_copy_auth(self) -> None:
1141client = OpenAI(base_url=base_url, api_key=lambda: "test_bearer_token_1").copy(
1142api_key=lambda: "test_bearer_token_2"
1143)
1144client._refresh_api_key()
1145assert client.auth_headers == {"Authorization": "Bearer test_bearer_token_2"}
1146
08b8179aDavid Schnurr2 years ago1147
1148class TestAsyncOpenAI:
1149@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1150async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
c5975bd0Stainless Bot2 years ago1151respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago1152
53278443stainless-app[bot]7 months ago1153response = await async_client.post("/foo", cast_to=httpx.Response)
08b8179aDavid Schnurr2 years ago1154assert response.status_code == 200
1155assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago1156assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago1157
1158@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1159async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1160respx_mock.post("/foo").mock(
1161return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
1162)
1163
53278443stainless-app[bot]7 months ago1164response = await async_client.post("/foo", cast_to=httpx.Response)
08b8179aDavid Schnurr2 years ago1165assert response.status_code == 200
1166assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago1167assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago1168
53278443stainless-app[bot]7 months ago1169def test_copy(self, async_client: AsyncOpenAI) -> None:
1170copied = async_client.copy()
1171assert id(copied) != id(async_client)
08b8179aDavid Schnurr2 years ago1172
53278443stainless-app[bot]7 months ago1173copied = async_client.copy(api_key="another My API Key")
08b8179aDavid Schnurr2 years ago1174assert copied.api_key == "another My API Key"
53278443stainless-app[bot]7 months ago1175assert async_client.api_key == "My API Key"
08b8179aDavid Schnurr2 years ago1176
53278443stainless-app[bot]7 months ago1177def test_copy_default_options(self, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1178# options that have a default are overridden correctly
53278443stainless-app[bot]7 months ago1179copied = async_client.copy(max_retries=7)
08b8179aDavid Schnurr2 years ago1180assert copied.max_retries == 7
53278443stainless-app[bot]7 months ago1181assert async_client.max_retries == 2
08b8179aDavid Schnurr2 years ago1182
1183copied2 = copied.copy(max_retries=6)
1184assert copied2.max_retries == 6
1185assert copied.max_retries == 7
1186
1187# timeout
53278443stainless-app[bot]7 months ago1188assert isinstance(async_client.timeout, httpx.Timeout)
1189copied = async_client.copy(timeout=None)
08b8179aDavid Schnurr2 years ago1190assert copied.timeout is None
53278443stainless-app[bot]7 months ago1191assert isinstance(async_client.timeout, httpx.Timeout)
08b8179aDavid Schnurr2 years ago1192
53278443stainless-app[bot]7 months ago1193async def test_copy_default_headers(self) -> None:
08b8179aDavid Schnurr2 years ago1194client = AsyncOpenAI(
1195base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1196)
1197assert client.default_headers["X-Foo"] == "bar"
1198
1199# does not override the already given value when not specified
1200copied = client.copy()
1201assert copied.default_headers["X-Foo"] == "bar"
1202
1203# merges already given headers
1204copied = client.copy(default_headers={"X-Bar": "stainless"})
1205assert copied.default_headers["X-Foo"] == "bar"
1206assert copied.default_headers["X-Bar"] == "stainless"
1207
1208# uses new values for any already given headers
1209copied = client.copy(default_headers={"X-Foo": "stainless"})
1210assert copied.default_headers["X-Foo"] == "stainless"
1211
1212# set_default_headers
1213
1214# completely overrides already set values
1215copied = client.copy(set_default_headers={})
1216assert copied.default_headers.get("X-Foo") is None
1217
1218copied = client.copy(set_default_headers={"X-Bar": "Robert"})
1219assert copied.default_headers["X-Bar"] == "Robert"
1220
1221with pytest.raises(
1222ValueError,
1223match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
1224):
1225client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
53278443stainless-app[bot]7 months ago1226await client.close()
08b8179aDavid Schnurr2 years ago1227
53278443stainless-app[bot]7 months ago1228async def test_copy_default_query(self) -> None:
08b8179aDavid Schnurr2 years ago1229client = AsyncOpenAI(
1230base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
1231)
1232assert _get_params(client)["foo"] == "bar"
1233
1234# does not override the already given value when not specified
1235copied = client.copy()
1236assert _get_params(copied)["foo"] == "bar"
1237
1238# merges already given params
1239copied = client.copy(default_query={"bar": "stainless"})
1240params = _get_params(copied)
1241assert params["foo"] == "bar"
1242assert params["bar"] == "stainless"
1243
1244# uses new values for any already given headers
1245copied = client.copy(default_query={"foo": "stainless"})
1246assert _get_params(copied)["foo"] == "stainless"
1247
1248# set_default_query
1249
1250# completely overrides already set values
1251copied = client.copy(set_default_query={})
1252assert _get_params(copied) == {}
1253
1254copied = client.copy(set_default_query={"bar": "Robert"})
1255assert _get_params(copied)["bar"] == "Robert"
1256
1257with pytest.raises(
1258ValueError,
1259# TODO: update
1260match="`default_query` and `set_default_query` arguments are mutually exclusive",
1261):
1262client.copy(set_default_query={}, default_query={"foo": "Bar"})
1263
53278443stainless-app[bot]7 months ago1264await client.close()
1265
1266def test_copy_signature(self, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1267# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
1268init_signature = inspect.signature(
1269# mypy doesn't like that we access the `__init__` property.
53278443stainless-app[bot]7 months ago1270async_client.__init__, # type: ignore[misc]
08b8179aDavid Schnurr2 years ago1271)
53278443stainless-app[bot]7 months ago1272copy_signature = inspect.signature(async_client.copy)
08b8179aDavid Schnurr2 years ago1273exclude_params = {"transport", "proxies", "_strict_response_validation"}
1274
1275for name in init_signature.parameters.keys():
1276if name in exclude_params:
1277continue
1278
1279copy_param = copy_signature.parameters.get(name)
1280assert copy_param is not None, f"copy() signature is missing the {name} param"
1281
18e0b36astainless-app[bot]1 years ago1282@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
53278443stainless-app[bot]7 months ago1283def test_copy_build_request(self, async_client: AsyncOpenAI) -> None:
d052708aStainless Bot2 years ago1284options = FinalRequestOptions(method="get", url="/foo")
1285
1286def build_request(options: FinalRequestOptions) -> None:
53278443stainless-app[bot]7 months ago1287client_copy = async_client.copy()
1288client_copy._build_request(options)
d052708aStainless Bot2 years ago1289
1290# ensure that the machinery is warmed up before tracing starts.
1291build_request(options)
1292gc.collect()
1293
1294tracemalloc.start(1000)
1295
1296snapshot_before = tracemalloc.take_snapshot()
1297
1298ITERATIONS = 10
1299for _ in range(ITERATIONS):
1300build_request(options)
1301
ce04ec28Stainless Bot2 years ago1302gc.collect()
d052708aStainless Bot2 years ago1303snapshot_after = tracemalloc.take_snapshot()
1304
1305tracemalloc.stop()
1306
1307def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
1308if diff.count == 0:
1309# Avoid false positives by considering only leaks (i.e. allocations that persist).
1310return
1311
1312if diff.count % ITERATIONS != 0:
1313# Avoid false positives by considering only leaks that appear per iteration.
1314return
1315
1316for frame in diff.traceback:
1317if any(
1318frame.filename.endswith(fragment)
1319for fragment in [
1320# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
1321#
1322# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago1323"openai/_legacy_response.py",
d052708aStainless Bot2 years ago1324"openai/_response.py",
1325# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
1326"openai/_compat.py",
1327# Standard library leaks we don't care about.
1328"/logging/__init__.py",
1329]
1330):
1331return
1332
1333leaks.append(diff)
1334
1335leaks: list[tracemalloc.StatisticDiff] = []
1336for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
1337add_leak(leaks, diff)
1338if leaks:
1339for leak in leaks:
1340print("MEMORY LEAK:", leak)
1341for frame in leak.traceback:
1342print(frame)
1343raise AssertionError()
1344
53278443stainless-app[bot]7 months ago1345async def test_request_timeout(self, async_client: AsyncOpenAI) -> None:
1346request = async_client._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago1347timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1348assert timeout == DEFAULT_TIMEOUT
1349
53278443stainless-app[bot]7 months ago1350request = async_client._build_request(
08b8179aDavid Schnurr2 years ago1351FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
1352)
1353timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1354assert timeout == httpx.Timeout(100.0)
1355
1356async def test_client_timeout_option(self) -> None:
1357client = AsyncOpenAI(
1358base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
1359)
1360
1361request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1362timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1363assert timeout == httpx.Timeout(0)
1364
53278443stainless-app[bot]7 months ago1365await client.close()
1366
08b8179aDavid Schnurr2 years ago1367async def test_http_client_timeout_option(self) -> None:
1368# custom timeout given to the httpx client should be used
1369async with httpx.AsyncClient(timeout=None) as http_client:
1370client = AsyncOpenAI(
1371base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1372)
1373
1374request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1375timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1376assert timeout == httpx.Timeout(None)
1377
53278443stainless-app[bot]7 months ago1378await client.close()
1379
08b8179aDavid Schnurr2 years ago1380# no timeout given to the httpx client should not use the httpx default
1381async with httpx.AsyncClient() as http_client:
1382client = AsyncOpenAI(
1383base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1384)
1385
1386request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1387timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1388assert timeout == DEFAULT_TIMEOUT
1389
53278443stainless-app[bot]7 months ago1390await client.close()
1391
08b8179aDavid Schnurr2 years ago1392# explicitly passing the default timeout currently results in it being ignored
1393async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
1394client = AsyncOpenAI(
1395base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1396)
1397
1398request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1399timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1400assert timeout == DEFAULT_TIMEOUT # our default
1401
53278443stainless-app[bot]7 months ago1402await client.close()
1403
dae0ec80Stainless Bot2 years ago1404def test_invalid_http_client(self) -> None:
1405with pytest.raises(TypeError, match="Invalid `http_client` arg"):
1406with httpx.Client() as http_client:
1407AsyncOpenAI(
1408base_url=base_url,
1409api_key=api_key,
1410_strict_response_validation=True,
1411http_client=cast(Any, http_client),
1412)
1413
53278443stainless-app[bot]7 months ago1414async def test_default_headers_option(self) -> None:
1415test_client = AsyncOpenAI(
08b8179aDavid Schnurr2 years ago1416base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1417)
53278443stainless-app[bot]7 months ago1418request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago1419assert request.headers.get("x-foo") == "bar"
1420assert request.headers.get("x-stainless-lang") == "python"
1421
53278443stainless-app[bot]7 months ago1422test_client2 = AsyncOpenAI(
08b8179aDavid Schnurr2 years ago1423base_url=base_url,
1424api_key=api_key,
1425_strict_response_validation=True,
1426default_headers={
1427"X-Foo": "stainless",
1428"X-Stainless-Lang": "my-overriding-header",
1429},
1430)
53278443stainless-app[bot]7 months ago1431request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago1432assert request.headers.get("x-foo") == "stainless"
1433assert request.headers.get("x-stainless-lang") == "my-overriding-header"
1434
53278443stainless-app[bot]7 months ago1435await test_client.close()
1436await test_client2.close()
1437
25d16be1Johan Stenberg (MSFT)9 months ago1438async def test_validate_headers(self) -> None:
08b8179aDavid Schnurr2 years ago1439client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
25d16be1Johan Stenberg (MSFT)9 months ago1440options = await client._prepare_options(FinalRequestOptions(method="get", url="/foo"))
1441request = client._build_request(options)
08b8179aDavid Schnurr2 years ago1442assert request.headers.get("Authorization") == f"Bearer {api_key}"
1443
e967f5a5Stainless Bot2 years ago1444with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago1445with update_env(**{"OPENAI_API_KEY": Omit()}):
1446client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago1447_ = client2
1448
53278443stainless-app[bot]7 months ago1449async def test_default_query_option(self) -> None:
08b8179aDavid Schnurr2 years ago1450client = AsyncOpenAI(
1451base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
1452)
1453request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1454url = httpx.URL(request.url)
1455assert dict(url.params) == {"query_param": "bar"}
1456
1457request = client._build_request(
1458FinalRequestOptions(
1459method="get",
1460url="/foo",
eba67815stainless-app[bot]1 years ago1461params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago1462)
1463)
1464url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago1465assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago1466
53278443stainless-app[bot]7 months ago1467await client.close()
1468
1469def test_request_extra_json(self, client: OpenAI) -> None:
1470request = client._build_request(
08b8179aDavid Schnurr2 years ago1471FinalRequestOptions(
1472method="post",
1473url="/foo",
1474json_data={"foo": "bar"},
1475extra_json={"baz": False},
1476),
1477)
1478data = json.loads(request.content.decode("utf-8"))
1479assert data == {"foo": "bar", "baz": False}
1480
53278443stainless-app[bot]7 months ago1481request = client._build_request(
08b8179aDavid Schnurr2 years ago1482FinalRequestOptions(
1483method="post",
1484url="/foo",
1485extra_json={"baz": False},
1486),
1487)
1488data = json.loads(request.content.decode("utf-8"))
1489assert data == {"baz": False}
1490
1491# `extra_json` takes priority over `json_data` when keys clash
53278443stainless-app[bot]7 months ago1492request = client._build_request(
08b8179aDavid Schnurr2 years ago1493FinalRequestOptions(
1494method="post",
1495url="/foo",
1496json_data={"foo": "bar", "baz": True},
1497extra_json={"baz": None},
1498),
1499)
1500data = json.loads(request.content.decode("utf-8"))
1501assert data == {"foo": "bar", "baz": None}
1502
53278443stainless-app[bot]7 months ago1503def test_request_extra_headers(self, client: OpenAI) -> None:
1504request = client._build_request(
08b8179aDavid Schnurr2 years ago1505FinalRequestOptions(
1506method="post",
1507url="/foo",
1508**make_request_options(extra_headers={"X-Foo": "Foo"}),
1509),
1510)
1511assert request.headers.get("X-Foo") == "Foo"
1512
1513# `extra_headers` takes priority over `default_headers` when keys clash
53278443stainless-app[bot]7 months ago1514request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
08b8179aDavid Schnurr2 years ago1515FinalRequestOptions(
1516method="post",
1517url="/foo",
1518**make_request_options(
1519extra_headers={"X-Bar": "false"},
1520),
1521),
1522)
1523assert request.headers.get("X-Bar") == "false"
1524
53278443stainless-app[bot]7 months ago1525def test_request_extra_query(self, client: OpenAI) -> None:
1526request = client._build_request(
08b8179aDavid Schnurr2 years ago1527FinalRequestOptions(
1528method="post",
1529url="/foo",
1530**make_request_options(
1531extra_query={"my_query_param": "Foo"},
1532),
1533),
1534)
31573844Stainless Bot2 years ago1535params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1536assert params == {"my_query_param": "Foo"}
1537
1538# if both `query` and `extra_query` are given, they are merged
53278443stainless-app[bot]7 months ago1539request = client._build_request(
08b8179aDavid Schnurr2 years ago1540FinalRequestOptions(
1541method="post",
1542url="/foo",
1543**make_request_options(
1544query={"bar": "1"},
1545extra_query={"foo": "2"},
1546),
1547),
1548)
31573844Stainless Bot2 years ago1549params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1550assert params == {"bar": "1", "foo": "2"}
1551
1552# `extra_query` takes priority over `query` when keys clash
53278443stainless-app[bot]7 months ago1553request = client._build_request(
08b8179aDavid Schnurr2 years ago1554FinalRequestOptions(
1555method="post",
1556url="/foo",
1557**make_request_options(
1558query={"foo": "1"},
1559extra_query={"foo": "2"},
1560),
1561),
1562)
31573844Stainless Bot2 years ago1563params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1564assert params == {"foo": "2"}
1565
22713fd0Stainless Bot2 years ago1566def test_multipart_repeating_array(self, async_client: AsyncOpenAI) -> None:
1567request = async_client._build_request(
1568FinalRequestOptions.construct(
fcbb5983stainless-app[bot]11 months ago1569method="post",
22713fd0Stainless Bot2 years ago1570url="/foo",
1571headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
1572json_data={"array": ["foo", "bar"]},
1573files=[("foo.txt", b"hello world")],
1574)
1575)
1576
1577assert request.read().split(b"\r\n") == [
1578b"--6b7ba517decee4a450543ea6ae821c82",
1579b'Content-Disposition: form-data; name="array[]"',
1580b"",
1581b"foo",
1582b"--6b7ba517decee4a450543ea6ae821c82",
1583b'Content-Disposition: form-data; name="array[]"',
1584b"",
1585b"bar",
1586b"--6b7ba517decee4a450543ea6ae821c82",
1587b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
1588b"Content-Type: application/octet-stream",
1589b"",
1590b"hello world",
1591b"--6b7ba517decee4a450543ea6ae821c82--",
1592b"",
1593]
1594
a532f6efstainless-app[bot]5 months ago1595@pytest.mark.respx(base_url=base_url)
1596async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
1597respx_mock.post("/upload").mock(side_effect=mirror_request_content)
1598
1599file_content = b"Hello, this is a test file."
1600
1601response = await async_client.post(
1602"/upload",
1603content=file_content,
1604cast_to=httpx.Response,
1605options={"headers": {"Content-Type": "application/octet-stream"}},
1606)
1607
1608assert response.status_code == 200
1609assert response.request.headers["Content-Type"] == "application/octet-stream"
1610assert response.content == file_content
1611
1612async def test_binary_content_upload_with_asynciterator(self) -> None:
1613file_content = b"Hello, this is a test file."
1614counter = Counter()
1615iterator = _make_async_iterator([file_content], counter=counter)
1616
1617async def mock_handler(request: httpx.Request) -> httpx.Response:
1618assert counter.value == 0, "the request body should not have been read"
1619return httpx.Response(200, content=await request.aread())
1620
1621async with AsyncOpenAI(
1622base_url=base_url,
1623api_key=api_key,
1624_strict_response_validation=True,
1625http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)),
1626) as client:
1627response = await client.post(
1628"/upload",
1629content=iterator,
1630cast_to=httpx.Response,
1631options={"headers": {"Content-Type": "application/octet-stream"}},
1632)
1633
1634assert response.status_code == 200
1635assert response.request.headers["Content-Type"] == "application/octet-stream"
1636assert response.content == file_content
1637assert counter.value == 1
1638
1639@pytest.mark.respx(base_url=base_url)
1640async def test_binary_content_upload_with_body_is_deprecated(
1641self, respx_mock: MockRouter, async_client: AsyncOpenAI
1642) -> None:
1643respx_mock.post("/upload").mock(side_effect=mirror_request_content)
1644
1645file_content = b"Hello, this is a test file."
1646
1647with pytest.deprecated_call(
1648match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
1649):
1650response = await async_client.post(
1651"/upload",
1652body=file_content,
1653cast_to=httpx.Response,
1654options={"headers": {"Content-Type": "application/octet-stream"}},
1655)
1656
1657assert response.status_code == 200
1658assert response.request.headers["Content-Type"] == "application/octet-stream"
1659assert response.content == file_content
1660
08b8179aDavid Schnurr2 years ago1661@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1662async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1663class Model1(BaseModel):
1664name: str
1665
1666class Model2(BaseModel):
1667foo: str
1668
1669respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1670
53278443stainless-app[bot]7 months ago1671response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago1672assert isinstance(response, Model2)
1673assert response.foo == "bar"
1674
1675@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1676async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1677"""Union of objects with the same field name using a different type"""
1678
1679class Model1(BaseModel):
1680foo: int
1681
1682class Model2(BaseModel):
1683foo: str
1684
1685respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1686
53278443stainless-app[bot]7 months ago1687response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago1688assert isinstance(response, Model2)
1689assert response.foo == "bar"
1690
1691respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1692
53278443stainless-app[bot]7 months ago1693response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago1694assert isinstance(response, Model1)
1695assert response.foo == 1
1696
c26014e2Stainless Bot2 years ago1697@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1698async def test_non_application_json_content_type_for_json_data(
1699self, respx_mock: MockRouter, async_client: AsyncOpenAI
1700) -> None:
c26014e2Stainless Bot2 years ago1701"""
1702Response that sets Content-Type to something other than application/json but returns json data
1703"""
1704
1705class Model(BaseModel):
1706foo: int
1707
1708respx_mock.get("/foo").mock(
1709return_value=httpx.Response(
1710200,
1711content=json.dumps({"foo": 2}),
1712headers={"Content-Type": "application/text"},
1713)
1714)
1715
53278443stainless-app[bot]7 months ago1716response = await async_client.get("/foo", cast_to=Model)
c26014e2Stainless Bot2 years ago1717assert isinstance(response, Model)
1718assert response.foo == 2
1719
53278443stainless-app[bot]7 months ago1720async def test_base_url_setter(self) -> None:
f6f38a9bStainless Bot2 years ago1721client = AsyncOpenAI(
1722base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1723)
1724assert client.base_url == "https://example.com/from_init/"
1725
1726client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1727
1728assert client.base_url == "https://example.com/from_setter/"
1729
53278443stainless-app[bot]7 months ago1730await client.close()
1731
1732async def test_base_url_env(self) -> None:
0733934fStainless Bot2 years ago1733with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1734client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1735assert client.base_url == "http://localhost:5000/from/env/"
1736
08b8179aDavid Schnurr2 years ago1737@pytest.mark.parametrize(
1738"client",
1739[
1740AsyncOpenAI(
1741base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1742),
1743AsyncOpenAI(
1744base_url="http://localhost:5000/custom/path/",
1745api_key=api_key,
1746_strict_response_validation=True,
1747http_client=httpx.AsyncClient(),
1748),
1749],
1750ids=["standard", "custom http client"],
1751)
53278443stainless-app[bot]7 months ago1752async def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1753request = client._build_request(
1754FinalRequestOptions(
1755method="post",
1756url="/foo",
1757json_data={"foo": "bar"},
1758),
1759)
1760assert request.url == "http://localhost:5000/custom/path/foo"
53278443stainless-app[bot]7 months ago1761await client.close()
08b8179aDavid Schnurr2 years ago1762
1763@pytest.mark.parametrize(
1764"client",
1765[
1766AsyncOpenAI(
1767base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1768),
1769AsyncOpenAI(
1770base_url="http://localhost:5000/custom/path/",
1771api_key=api_key,
1772_strict_response_validation=True,
1773http_client=httpx.AsyncClient(),
1774),
1775],
1776ids=["standard", "custom http client"],
1777)
53278443stainless-app[bot]7 months ago1778async def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1779request = client._build_request(
1780FinalRequestOptions(
1781method="post",
1782url="/foo",
1783json_data={"foo": "bar"},
1784),
1785)
1786assert request.url == "http://localhost:5000/custom/path/foo"
53278443stainless-app[bot]7 months ago1787await client.close()
08b8179aDavid Schnurr2 years ago1788
1789@pytest.mark.parametrize(
1790"client",
1791[
1792AsyncOpenAI(
1793base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1794),
1795AsyncOpenAI(
1796base_url="http://localhost:5000/custom/path/",
1797api_key=api_key,
1798_strict_response_validation=True,
1799http_client=httpx.AsyncClient(),
1800),
1801],
1802ids=["standard", "custom http client"],
1803)
53278443stainless-app[bot]7 months ago1804async def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1805request = client._build_request(
1806FinalRequestOptions(
1807method="post",
1808url="https://myapi.com/foo",
1809json_data={"foo": "bar"},
1810),
1811)
1812assert request.url == "https://myapi.com/foo"
53278443stainless-app[bot]7 months ago1813await client.close()
08b8179aDavid Schnurr2 years ago1814
1815async def test_copied_client_does_not_close_http(self) -> None:
53278443stainless-app[bot]7 months ago1816test_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1817assert not test_client.is_closed()
08b8179aDavid Schnurr2 years ago1818
53278443stainless-app[bot]7 months ago1819copied = test_client.copy()
1820assert copied is not test_client
08b8179aDavid Schnurr2 years ago1821
a7ebc260Stainless Bot2 years ago1822del copied
08b8179aDavid Schnurr2 years ago1823
1824await asyncio.sleep(0.2)
53278443stainless-app[bot]7 months ago1825assert not test_client.is_closed()
08b8179aDavid Schnurr2 years ago1826
1827async def test_client_context_manager(self) -> None:
53278443stainless-app[bot]7 months ago1828test_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1829async with test_client as c2:
1830assert c2 is test_client
08b8179aDavid Schnurr2 years ago1831assert not c2.is_closed()
53278443stainless-app[bot]7 months ago1832assert not test_client.is_closed()
1833assert test_client.is_closed()
08b8179aDavid Schnurr2 years ago1834
1835@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1836async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1837class Model(BaseModel):
1838foo: str
1839
1840respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1841
1842with pytest.raises(APIResponseValidationError) as exc:
53278443stainless-app[bot]7 months ago1843await async_client.get("/foo", cast_to=Model)
08b8179aDavid Schnurr2 years ago1844
1845assert isinstance(exc.value.__cause__, ValidationError)
1846
07079085Stainless Bot2 years ago1847async def test_client_max_retries_validation(self) -> None:
1848with pytest.raises(TypeError, match=r"max_retries cannot be None"):
1849AsyncOpenAI(
1850base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)
1851)
1852
08b8179aDavid Schnurr2 years ago1853@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago1854async def test_default_stream_cls(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1855class Model(BaseModel):
1856name: str
1857
1858respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1859
53278443stainless-app[bot]7 months ago1860stream = await async_client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
86379b44Stainless Bot2 years ago1861assert isinstance(stream, AsyncStream)
1862await stream.response.aclose()
08b8179aDavid Schnurr2 years ago1863
1864@pytest.mark.respx(base_url=base_url)
1865async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1866class Model(BaseModel):
1867name: str
1868
1869respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1870
1871strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1872
1873with pytest.raises(APIResponseValidationError):
1874await strict_client.get("/foo", cast_to=Model)
1875
53278443stainless-app[bot]7 months ago1876non_strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
08b8179aDavid Schnurr2 years ago1877
53278443stainless-app[bot]7 months ago1878response = await non_strict_client.get("/foo", cast_to=Model)
08b8179aDavid Schnurr2 years ago1879assert isinstance(response, str) # type: ignore[unreachable]
1880
53278443stainless-app[bot]7 months ago1881await strict_client.close()
1882await non_strict_client.close()
1883
08b8179aDavid Schnurr2 years ago1884@pytest.mark.parametrize(
1885"remaining_retries,retry_after,timeout",
1886[
1887[3, "20", 20],
1888[3, "0", 0.5],
1889[3, "-10", 0.5],
1890[3, "60", 60],
1891[3, "61", 0.5],
1892[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1893[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1894[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1895[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1896[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1897[3, "99999999999999999999999999999999999", 0.5],
1898[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1899[3, "", 0.5],
1900[2, "", 0.5 * 2.0],
1901[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago1902[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago1903],
1904)
1905@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
53278443stainless-app[bot]7 months ago1906async def test_parse_retry_after_header(
1907self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncOpenAI
1908) -> None:
08b8179aDavid Schnurr2 years ago1909headers = httpx.Headers({"retry-after": retry_after})
1910options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
53278443stainless-app[bot]7 months ago1911calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
08b8179aDavid Schnurr2 years ago1912assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
7aad3405Stainless Bot2 years ago1913
ba4f7a97Stainless Bot2 years ago1914@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1915@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1916async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1917respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
1918
1919with pytest.raises(APITimeoutError):
0bef1d02stainless-app[bot]1 years ago1920await async_client.chat.completions.with_streaming_response.create(
1921messages=[
1922{
1923"content": "string",
1924"role": "developer",
1925}
1926],
9b1bb6eestainless-app[bot]3 months ago1927model="gpt-5.4",
0bef1d02stainless-app[bot]1 years ago1928).__aenter__()
7aad3405Stainless Bot2 years ago1929
53278443stainless-app[bot]7 months ago1930assert _get_open_connections(async_client) == 0
7aad3405Stainless Bot2 years ago1931
ba4f7a97Stainless Bot2 years ago1932@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago1933@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1934async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1935respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago1936
ba4f7a97Stainless Bot2 years ago1937with pytest.raises(APIStatusError):
0bef1d02stainless-app[bot]1 years ago1938await async_client.chat.completions.with_streaming_response.create(
1939messages=[
1940{
1941"content": "string",
1942"role": "developer",
1943}
1944],
9b1bb6eestainless-app[bot]3 months ago1945model="gpt-5.4",
0bef1d02stainless-app[bot]1 years ago1946).__aenter__()
53278443stainless-app[bot]7 months ago1947assert _get_open_connections(async_client) == 0
98d8b2acstainless-app[bot]1 years ago1948
1949@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1950@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1951@pytest.mark.respx(base_url=base_url)
6634f525stainless-app[bot]1 years ago1952@pytest.mark.parametrize("failure_mode", ["status", "exception"])
98d8b2acstainless-app[bot]1 years ago1953async def test_retries_taken(
6634f525stainless-app[bot]1 years ago1954self,
1955async_client: AsyncOpenAI,
1956failures_before_success: int,
1957failure_mode: Literal["status", "exception"],
1958respx_mock: MockRouter,
98d8b2acstainless-app[bot]1 years ago1959) -> None:
1960client = async_client.with_options(max_retries=4)
1961
1962nb_retries = 0
1963
1964def retry_handler(_request: httpx.Request) -> httpx.Response:
1965nonlocal nb_retries
1966if nb_retries < failures_before_success:
1967nb_retries += 1
6634f525stainless-app[bot]1 years ago1968if failure_mode == "exception":
1969raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago1970return httpx.Response(500)
1971return httpx.Response(200)
1972
1973respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1974
1975response = await client.chat.completions.with_raw_response.create(
1976messages=[
1977{
bf1ca86cRobert Craigie1 years ago1978"content": "string",
575ff607stainless-app[bot]1 years ago1979"role": "developer",
98d8b2acstainless-app[bot]1 years ago1980}
1981],
9b1bb6eestainless-app[bot]3 months ago1982model="gpt-5.4",
98d8b2acstainless-app[bot]1 years ago1983)
1984
1985assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1986assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
1987
1988@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1989@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1990@pytest.mark.respx(base_url=base_url)
1991async def test_omit_retry_count_header(
1992self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1993) -> None:
1994client = async_client.with_options(max_retries=4)
1995
1996nb_retries = 0
1997
1998def retry_handler(_request: httpx.Request) -> httpx.Response:
1999nonlocal nb_retries
2000if nb_retries < failures_before_success:
2001nb_retries += 1
2002return httpx.Response(500)
2003return httpx.Response(200)
2004
2005respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
2006
2007response = await client.chat.completions.with_raw_response.create(
2008messages=[
2009{
2010"content": "string",
575ff607stainless-app[bot]1 years ago2011"role": "developer",
5449e208Stainless Bot1 years ago2012}
2013],
9b1bb6eestainless-app[bot]3 months ago2014model="gpt-5.4",
5449e208Stainless Bot1 years ago2015extra_headers={"x-stainless-retry-count": Omit()},
2016)
2017
2018assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
2019
2020@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
2021@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
2022@pytest.mark.respx(base_url=base_url)
2023async def test_overwrite_retry_count_header(
2024self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
2025) -> None:
2026client = async_client.with_options(max_retries=4)
2027
2028nb_retries = 0
2029
2030def retry_handler(_request: httpx.Request) -> httpx.Response:
2031nonlocal nb_retries
2032if nb_retries < failures_before_success:
2033nb_retries += 1
2034return httpx.Response(500)
2035return httpx.Response(200)
2036
2037respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
2038
2039response = await client.chat.completions.with_raw_response.create(
2040messages=[
2041{
2042"content": "string",
575ff607stainless-app[bot]1 years ago2043"role": "developer",
5449e208Stainless Bot1 years ago2044}
2045],
9b1bb6eestainless-app[bot]3 months ago2046model="gpt-5.4",
5449e208Stainless Bot1 years ago2047extra_headers={"x-stainless-retry-count": "42"},
2048)
2049
2050assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago2051
2052@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
2053@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
2054@pytest.mark.respx(base_url=base_url)
2055async def test_retries_taken_new_response_class(
2056self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
2057) -> None:
2058client = async_client.with_options(max_retries=4)
2059
2060nb_retries = 0
2061
2062def retry_handler(_request: httpx.Request) -> httpx.Response:
2063nonlocal nb_retries
2064if nb_retries < failures_before_success:
2065nb_retries += 1
2066return httpx.Response(500)
2067return httpx.Response(200)
2068
2069respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
2070
2071async with client.chat.completions.with_streaming_response.create(
2072messages=[
2073{
bf1ca86cRobert Craigie1 years ago2074"content": "string",
575ff607stainless-app[bot]1 years ago2075"role": "developer",
98d8b2acstainless-app[bot]1 years ago2076}
2077],
9b1bb6eestainless-app[bot]3 months ago2078model="gpt-5.4",
98d8b2acstainless-app[bot]1 years ago2079) as response:
2080assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago2081assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
d8901d28Seth Gilchrist1 years ago2082
4756247cstainless-app[bot]9 months ago2083async def test_get_platform(self) -> None:
2084platform = await asyncify(get_platform)()
2085assert isinstance(platform, (str, OtherPlatform))
cca09707stainless-app[bot]1 years ago2086
cc2c1fc1stainless-app[bot]1 years ago2087async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
2088# Test that the proxy environment variables are set correctly
2089monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
656e3cabstainless-app[bot]4 months ago2090# Delete in case our environment has any proxy env vars set
588d239cstainless-app[bot]4 months ago2091monkeypatch.delenv("HTTP_PROXY", raising=False)
656e3cabstainless-app[bot]4 months ago2092monkeypatch.delenv("ALL_PROXY", raising=False)
2093monkeypatch.delenv("NO_PROXY", raising=False)
2094monkeypatch.delenv("http_proxy", raising=False)
2095monkeypatch.delenv("https_proxy", raising=False)
2096monkeypatch.delenv("all_proxy", raising=False)
2097monkeypatch.delenv("no_proxy", raising=False)
cc2c1fc1stainless-app[bot]1 years ago2098
2099client = DefaultAsyncHttpxClient()
2100
2101mounts = tuple(client._mounts.items())
2102assert len(mounts) == 1
2103assert mounts[0][0].pattern == "https://"
2104
2105@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
2106async def test_default_client_creation(self) -> None:
2107# Ensure that the client can be initialized without any exceptions
2108DefaultAsyncHttpxClient(
2109verify=True,
2110cert=None,
2111trust_env=True,
2112http1=True,
2113http2=False,
2114limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
2115)
2116
cca09707stainless-app[bot]1 years ago2117@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago2118async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
cca09707stainless-app[bot]1 years ago2119# Test that the default follow_redirects=True allows following redirects
2120respx_mock.post("/redirect").mock(
2121return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
2122)
2123respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
2124
53278443stainless-app[bot]7 months ago2125response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
cca09707stainless-app[bot]1 years ago2126assert response.status_code == 200
2127assert response.json() == {"status": "ok"}
2128
2129@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]7 months ago2130async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
cca09707stainless-app[bot]1 years ago2131# Test that follow_redirects=False prevents following redirects
2132respx_mock.post("/redirect").mock(
2133return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
2134)
2135
2136with pytest.raises(APIStatusError) as exc_info:
53278443stainless-app[bot]7 months ago2137await async_client.post(
cca09707stainless-app[bot]1 years ago2138"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
2139)
2140
2141assert exc_info.value.response.status_code == 302
2142assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
25d16be1Johan Stenberg (MSFT)9 months ago2143
2144@pytest.mark.asyncio
2145async def test_api_key_before_after_refresh_provider(self) -> None:
2146async def mock_api_key_provider():
2147return "test_bearer_token"
2148
2149client = AsyncOpenAI(base_url=base_url, api_key=mock_api_key_provider)
2150
2151assert client.api_key == ""
2152assert "Authorization" not in client.auth_headers
2153
2154await client._refresh_api_key()
2155
2156assert client.api_key == "test_bearer_token"
2157assert client.auth_headers.get("Authorization") == "Bearer test_bearer_token"
2158
2159@pytest.mark.asyncio
2160async def test_api_key_before_after_refresh_str(self) -> None:
2161client = AsyncOpenAI(base_url=base_url, api_key="test_api_key")
2162
2163assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
2164await client._refresh_api_key()
2165
2166assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
2167
2168@pytest.mark.asyncio
2169@pytest.mark.respx()
2170async def test_bearer_token_refresh_async(self, respx_mock: MockRouter) -> None:
2171respx_mock.post(base_url + "/chat/completions").mock(
2172side_effect=[
2173httpx.Response(500, json={"error": "server error"}),
2174httpx.Response(200, json={"foo": "bar"}),
2175]
2176)
2177
2178counter = 0
2179
2180async def token_provider() -> str:
2181nonlocal counter
2182
2183counter += 1
2184
2185if counter == 1:
2186return "first"
2187
2188return "second"
2189
2190client = AsyncOpenAI(base_url=base_url, api_key=token_provider)
2191await client.chat.completions.create(messages=[], model="gpt-4")
2192
2193calls = cast("list[MockRequestCall]", respx_mock.calls)
2194assert len(calls) == 2
2195
2196assert calls[0].request.headers.get("Authorization") == "Bearer first"
2197assert calls[1].request.headers.get("Authorization") == "Bearer second"
2198
2199@pytest.mark.asyncio
2200async def test_copy_auth(self) -> None:
2201async def token_provider_1() -> str:
2202return "test_bearer_token_1"
2203
2204async def token_provider_2() -> str:
2205return "test_bearer_token_2"
2206
2207client = AsyncOpenAI(base_url=base_url, api_key=token_provider_1).copy(api_key=token_provider_2)
2208await client._refresh_api_key()
2209assert client.auth_headers == {"Authorization": "Bearer test_bearer_token_2"}