openai/openai-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v2.18.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

tests/test_client.py

2193lines · 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]8 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]8 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]8 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]8 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]8 months ago140def test_copy(self, client: OpenAI) -> None:
141copied = client.copy()
142assert id(copied) != id(client)
08b8179aDavid Schnurr2 years ago143
53278443stainless-app[bot]8 months ago144copied = client.copy(api_key="another My API Key")
08b8179aDavid Schnurr2 years ago145assert copied.api_key == "another My API Key"
53278443stainless-app[bot]8 months ago146assert client.api_key == "My API Key"
08b8179aDavid Schnurr2 years ago147
53278443stainless-app[bot]8 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]8 months ago150copied = client.copy(max_retries=7)
08b8179aDavid Schnurr2 years ago151assert copied.max_retries == 7
53278443stainless-app[bot]8 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]8 months ago159assert isinstance(client.timeout, httpx.Timeout)
160copied = client.copy(timeout=None)
08b8179aDavid Schnurr2 years ago161assert copied.timeout is None
53278443stainless-app[bot]8 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]8 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]8 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]8 months ago241client.__init__, # type: ignore[misc]
08b8179aDavid Schnurr2 years ago242)
53278443stainless-app[bot]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 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]8 months ago769client.close()
08b8179aDavid Schnurr2 years ago770
771def test_copied_client_does_not_close_http(self) -> None:
53278443stainless-app[bot]8 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]8 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]8 months ago780assert not test_client.is_closed()
08b8179aDavid Schnurr2 years ago781
782def test_client_context_manager(self) -> None:
53278443stainless-app[bot]8 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]8 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]8 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]8 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]8 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]8 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]8 months ago829non_strict_client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
08b8179aDavid Schnurr2 years ago830
53278443stainless-app[bot]8 months ago831response = non_strict_client.get("/foo", cast_to=Model)
08b8179aDavid Schnurr2 years ago832assert isinstance(response, str) # type: ignore[unreachable]
833
53278443stainless-app[bot]8 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]8 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],
880model="gpt-4o",
881).__enter__()
7aad3405Stainless Bot2 years ago882
53278443stainless-app[bot]8 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],
898model="gpt-4o",
899).__enter__()
53278443stainless-app[bot]8 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],
bf1ca86cRobert Craigie1 years ago935model="gpt-4o",
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],
967model="gpt-4o",
968extra_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],
999model="gpt-4o",
1000extra_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],
bf1ca86cRobert Craigie1 years ago1031model="gpt-4o",
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")
1039
1040client = DefaultHttpxClient()
1041
1042mounts = tuple(client._mounts.items())
1043assert len(mounts) == 1
1044assert mounts[0][0].pattern == "https://"
1045
1046@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
1047def test_default_client_creation(self) -> None:
1048# Ensure that the client can be initialized without any exceptions
1049DefaultHttpxClient(
1050verify=True,
1051cert=None,
1052trust_env=True,
1053http1=True,
1054http2=False,
1055limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
1056)
1057
cca09707stainless-app[bot]1 years ago1058@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1059def test_follow_redirects(self, respx_mock: MockRouter, client: OpenAI) -> None:
cca09707stainless-app[bot]1 years ago1060# Test that the default follow_redirects=True allows following redirects
1061respx_mock.post("/redirect").mock(
1062return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1063)
1064respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1065
53278443stainless-app[bot]8 months ago1066response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
cca09707stainless-app[bot]1 years ago1067assert response.status_code == 200
1068assert response.json() == {"status": "ok"}
1069
1070@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1071def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: OpenAI) -> None:
cca09707stainless-app[bot]1 years ago1072# Test that follow_redirects=False prevents following redirects
1073respx_mock.post("/redirect").mock(
1074return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1075)
1076
1077with pytest.raises(APIStatusError) as exc_info:
53278443stainless-app[bot]8 months ago1078client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response)
cca09707stainless-app[bot]1 years ago1079
1080assert exc_info.value.response.status_code == 302
1081assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
1082
25d16be1Johan Stenberg (MSFT)9 months ago1083def test_api_key_before_after_refresh_provider(self) -> None:
1084client = OpenAI(base_url=base_url, api_key=lambda: "test_bearer_token")
1085
1086assert client.api_key == ""
1087assert "Authorization" not in client.auth_headers
1088
1089client._refresh_api_key()
1090
1091assert client.api_key == "test_bearer_token"
1092assert client.auth_headers.get("Authorization") == "Bearer test_bearer_token"
1093
1094def test_api_key_before_after_refresh_str(self) -> None:
1095client = OpenAI(base_url=base_url, api_key="test_api_key")
1096
1097assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
1098client._refresh_api_key()
1099
1100assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
1101
1102@pytest.mark.respx()
1103def test_api_key_refresh_on_retry(self, respx_mock: MockRouter) -> None:
1104respx_mock.post(base_url + "/chat/completions").mock(
1105side_effect=[
1106httpx.Response(500, json={"error": "server error"}),
1107httpx.Response(200, json={"foo": "bar"}),
1108]
1109)
1110
1111counter = 0
1112
1113def token_provider() -> str:
1114nonlocal counter
1115
1116counter += 1
1117
1118if counter == 1:
1119return "first"
1120
1121return "second"
1122
1123client = OpenAI(base_url=base_url, api_key=token_provider)
1124client.chat.completions.create(messages=[], model="gpt-4")
1125
1126calls = cast("list[MockRequestCall]", respx_mock.calls)
1127assert len(calls) == 2
1128
1129assert calls[0].request.headers.get("Authorization") == "Bearer first"
1130assert calls[1].request.headers.get("Authorization") == "Bearer second"
1131
1132def test_copy_auth(self) -> None:
1133client = OpenAI(base_url=base_url, api_key=lambda: "test_bearer_token_1").copy(
1134api_key=lambda: "test_bearer_token_2"
1135)
1136client._refresh_api_key()
1137assert client.auth_headers == {"Authorization": "Bearer test_bearer_token_2"}
1138
08b8179aDavid Schnurr2 years ago1139
1140class TestAsyncOpenAI:
1141@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1142async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
c5975bd0Stainless Bot2 years ago1143respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
08b8179aDavid Schnurr2 years ago1144
53278443stainless-app[bot]8 months ago1145response = await async_client.post("/foo", cast_to=httpx.Response)
08b8179aDavid Schnurr2 years ago1146assert response.status_code == 200
1147assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago1148assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago1149
1150@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1151async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1152respx_mock.post("/foo").mock(
1153return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
1154)
1155
53278443stainless-app[bot]8 months ago1156response = await async_client.post("/foo", cast_to=httpx.Response)
08b8179aDavid Schnurr2 years ago1157assert response.status_code == 200
1158assert isinstance(response, httpx.Response)
c5975bd0Stainless Bot2 years ago1159assert response.json() == {"foo": "bar"}
08b8179aDavid Schnurr2 years ago1160
53278443stainless-app[bot]8 months ago1161def test_copy(self, async_client: AsyncOpenAI) -> None:
1162copied = async_client.copy()
1163assert id(copied) != id(async_client)
08b8179aDavid Schnurr2 years ago1164
53278443stainless-app[bot]8 months ago1165copied = async_client.copy(api_key="another My API Key")
08b8179aDavid Schnurr2 years ago1166assert copied.api_key == "another My API Key"
53278443stainless-app[bot]8 months ago1167assert async_client.api_key == "My API Key"
08b8179aDavid Schnurr2 years ago1168
53278443stainless-app[bot]8 months ago1169def test_copy_default_options(self, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1170# options that have a default are overridden correctly
53278443stainless-app[bot]8 months ago1171copied = async_client.copy(max_retries=7)
08b8179aDavid Schnurr2 years ago1172assert copied.max_retries == 7
53278443stainless-app[bot]8 months ago1173assert async_client.max_retries == 2
08b8179aDavid Schnurr2 years ago1174
1175copied2 = copied.copy(max_retries=6)
1176assert copied2.max_retries == 6
1177assert copied.max_retries == 7
1178
1179# timeout
53278443stainless-app[bot]8 months ago1180assert isinstance(async_client.timeout, httpx.Timeout)
1181copied = async_client.copy(timeout=None)
08b8179aDavid Schnurr2 years ago1182assert copied.timeout is None
53278443stainless-app[bot]8 months ago1183assert isinstance(async_client.timeout, httpx.Timeout)
08b8179aDavid Schnurr2 years ago1184
53278443stainless-app[bot]8 months ago1185async def test_copy_default_headers(self) -> None:
08b8179aDavid Schnurr2 years ago1186client = AsyncOpenAI(
1187base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1188)
1189assert client.default_headers["X-Foo"] == "bar"
1190
1191# does not override the already given value when not specified
1192copied = client.copy()
1193assert copied.default_headers["X-Foo"] == "bar"
1194
1195# merges already given headers
1196copied = client.copy(default_headers={"X-Bar": "stainless"})
1197assert copied.default_headers["X-Foo"] == "bar"
1198assert copied.default_headers["X-Bar"] == "stainless"
1199
1200# uses new values for any already given headers
1201copied = client.copy(default_headers={"X-Foo": "stainless"})
1202assert copied.default_headers["X-Foo"] == "stainless"
1203
1204# set_default_headers
1205
1206# completely overrides already set values
1207copied = client.copy(set_default_headers={})
1208assert copied.default_headers.get("X-Foo") is None
1209
1210copied = client.copy(set_default_headers={"X-Bar": "Robert"})
1211assert copied.default_headers["X-Bar"] == "Robert"
1212
1213with pytest.raises(
1214ValueError,
1215match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
1216):
1217client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
53278443stainless-app[bot]8 months ago1218await client.close()
08b8179aDavid Schnurr2 years ago1219
53278443stainless-app[bot]8 months ago1220async def test_copy_default_query(self) -> None:
08b8179aDavid Schnurr2 years ago1221client = AsyncOpenAI(
1222base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
1223)
1224assert _get_params(client)["foo"] == "bar"
1225
1226# does not override the already given value when not specified
1227copied = client.copy()
1228assert _get_params(copied)["foo"] == "bar"
1229
1230# merges already given params
1231copied = client.copy(default_query={"bar": "stainless"})
1232params = _get_params(copied)
1233assert params["foo"] == "bar"
1234assert params["bar"] == "stainless"
1235
1236# uses new values for any already given headers
1237copied = client.copy(default_query={"foo": "stainless"})
1238assert _get_params(copied)["foo"] == "stainless"
1239
1240# set_default_query
1241
1242# completely overrides already set values
1243copied = client.copy(set_default_query={})
1244assert _get_params(copied) == {}
1245
1246copied = client.copy(set_default_query={"bar": "Robert"})
1247assert _get_params(copied)["bar"] == "Robert"
1248
1249with pytest.raises(
1250ValueError,
1251# TODO: update
1252match="`default_query` and `set_default_query` arguments are mutually exclusive",
1253):
1254client.copy(set_default_query={}, default_query={"foo": "Bar"})
1255
53278443stainless-app[bot]8 months ago1256await client.close()
1257
1258def test_copy_signature(self, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1259# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
1260init_signature = inspect.signature(
1261# mypy doesn't like that we access the `__init__` property.
53278443stainless-app[bot]8 months ago1262async_client.__init__, # type: ignore[misc]
08b8179aDavid Schnurr2 years ago1263)
53278443stainless-app[bot]8 months ago1264copy_signature = inspect.signature(async_client.copy)
08b8179aDavid Schnurr2 years ago1265exclude_params = {"transport", "proxies", "_strict_response_validation"}
1266
1267for name in init_signature.parameters.keys():
1268if name in exclude_params:
1269continue
1270
1271copy_param = copy_signature.parameters.get(name)
1272assert copy_param is not None, f"copy() signature is missing the {name} param"
1273
18e0b36astainless-app[bot]1 years ago1274@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
53278443stainless-app[bot]8 months ago1275def test_copy_build_request(self, async_client: AsyncOpenAI) -> None:
d052708aStainless Bot2 years ago1276options = FinalRequestOptions(method="get", url="/foo")
1277
1278def build_request(options: FinalRequestOptions) -> None:
53278443stainless-app[bot]8 months ago1279client_copy = async_client.copy()
1280client_copy._build_request(options)
d052708aStainless Bot2 years ago1281
1282# ensure that the machinery is warmed up before tracing starts.
1283build_request(options)
1284gc.collect()
1285
1286tracemalloc.start(1000)
1287
1288snapshot_before = tracemalloc.take_snapshot()
1289
1290ITERATIONS = 10
1291for _ in range(ITERATIONS):
1292build_request(options)
1293
ce04ec28Stainless Bot2 years ago1294gc.collect()
d052708aStainless Bot2 years ago1295snapshot_after = tracemalloc.take_snapshot()
1296
1297tracemalloc.stop()
1298
1299def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None:
1300if diff.count == 0:
1301# Avoid false positives by considering only leaks (i.e. allocations that persist).
1302return
1303
1304if diff.count % ITERATIONS != 0:
1305# Avoid false positives by considering only leaks that appear per iteration.
1306return
1307
1308for frame in diff.traceback:
1309if any(
1310frame.filename.endswith(fragment)
1311for fragment in [
1312# to_raw_response_wrapper leaks through the @functools.wraps() decorator.
1313#
1314# removing the decorator fixes the leak for reasons we don't understand.
86379b44Stainless Bot2 years ago1315"openai/_legacy_response.py",
d052708aStainless Bot2 years ago1316"openai/_response.py",
1317# pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason.
1318"openai/_compat.py",
1319# Standard library leaks we don't care about.
1320"/logging/__init__.py",
1321]
1322):
1323return
1324
1325leaks.append(diff)
1326
1327leaks: list[tracemalloc.StatisticDiff] = []
1328for diff in snapshot_after.compare_to(snapshot_before, "traceback"):
1329add_leak(leaks, diff)
1330if leaks:
1331for leak in leaks:
1332print("MEMORY LEAK:", leak)
1333for frame in leak.traceback:
1334print(frame)
1335raise AssertionError()
1336
53278443stainless-app[bot]8 months ago1337async def test_request_timeout(self, async_client: AsyncOpenAI) -> None:
1338request = async_client._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago1339timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1340assert timeout == DEFAULT_TIMEOUT
1341
53278443stainless-app[bot]8 months ago1342request = async_client._build_request(
08b8179aDavid Schnurr2 years ago1343FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
1344)
1345timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1346assert timeout == httpx.Timeout(100.0)
1347
1348async def test_client_timeout_option(self) -> None:
1349client = AsyncOpenAI(
1350base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)
1351)
1352
1353request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1354timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1355assert timeout == httpx.Timeout(0)
1356
53278443stainless-app[bot]8 months ago1357await client.close()
1358
08b8179aDavid Schnurr2 years ago1359async def test_http_client_timeout_option(self) -> None:
1360# custom timeout given to the httpx client should be used
1361async with httpx.AsyncClient(timeout=None) as http_client:
1362client = AsyncOpenAI(
1363base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1364)
1365
1366request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1367timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1368assert timeout == httpx.Timeout(None)
1369
53278443stainless-app[bot]8 months ago1370await client.close()
1371
08b8179aDavid Schnurr2 years ago1372# no timeout given to the httpx client should not use the httpx default
1373async with httpx.AsyncClient() as http_client:
1374client = AsyncOpenAI(
1375base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1376)
1377
1378request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1379timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1380assert timeout == DEFAULT_TIMEOUT
1381
53278443stainless-app[bot]8 months ago1382await client.close()
1383
08b8179aDavid Schnurr2 years ago1384# explicitly passing the default timeout currently results in it being ignored
1385async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
1386client = AsyncOpenAI(
1387base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client
1388)
1389
1390request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1391timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
1392assert timeout == DEFAULT_TIMEOUT # our default
1393
53278443stainless-app[bot]8 months ago1394await client.close()
1395
dae0ec80Stainless Bot2 years ago1396def test_invalid_http_client(self) -> None:
1397with pytest.raises(TypeError, match="Invalid `http_client` arg"):
1398with httpx.Client() as http_client:
1399AsyncOpenAI(
1400base_url=base_url,
1401api_key=api_key,
1402_strict_response_validation=True,
1403http_client=cast(Any, http_client),
1404)
1405
53278443stainless-app[bot]8 months ago1406async def test_default_headers_option(self) -> None:
1407test_client = AsyncOpenAI(
08b8179aDavid Schnurr2 years ago1408base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
1409)
53278443stainless-app[bot]8 months ago1410request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago1411assert request.headers.get("x-foo") == "bar"
1412assert request.headers.get("x-stainless-lang") == "python"
1413
53278443stainless-app[bot]8 months ago1414test_client2 = AsyncOpenAI(
08b8179aDavid Schnurr2 years ago1415base_url=base_url,
1416api_key=api_key,
1417_strict_response_validation=True,
1418default_headers={
1419"X-Foo": "stainless",
1420"X-Stainless-Lang": "my-overriding-header",
1421},
1422)
53278443stainless-app[bot]8 months ago1423request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
08b8179aDavid Schnurr2 years ago1424assert request.headers.get("x-foo") == "stainless"
1425assert request.headers.get("x-stainless-lang") == "my-overriding-header"
1426
53278443stainless-app[bot]8 months ago1427await test_client.close()
1428await test_client2.close()
1429
25d16be1Johan Stenberg (MSFT)9 months ago1430async def test_validate_headers(self) -> None:
08b8179aDavid Schnurr2 years ago1431client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
25d16be1Johan Stenberg (MSFT)9 months ago1432options = await client._prepare_options(FinalRequestOptions(method="get", url="/foo"))
1433request = client._build_request(options)
08b8179aDavid Schnurr2 years ago1434assert request.headers.get("Authorization") == f"Bearer {api_key}"
1435
e967f5a5Stainless Bot2 years ago1436with pytest.raises(OpenAIError):
30194f19stainless-app[bot]1 years ago1437with update_env(**{"OPENAI_API_KEY": Omit()}):
1438client2 = AsyncOpenAI(base_url=base_url, api_key=None, _strict_response_validation=True)
08b8179aDavid Schnurr2 years ago1439_ = client2
1440
53278443stainless-app[bot]8 months ago1441async def test_default_query_option(self) -> None:
08b8179aDavid Schnurr2 years ago1442client = AsyncOpenAI(
1443base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
1444)
1445request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
1446url = httpx.URL(request.url)
1447assert dict(url.params) == {"query_param": "bar"}
1448
1449request = client._build_request(
1450FinalRequestOptions(
1451method="get",
1452url="/foo",
eba67815stainless-app[bot]1 years ago1453params={"foo": "baz", "query_param": "overridden"},
08b8179aDavid Schnurr2 years ago1454)
1455)
1456url = httpx.URL(request.url)
eba67815stainless-app[bot]1 years ago1457assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
08b8179aDavid Schnurr2 years ago1458
53278443stainless-app[bot]8 months ago1459await client.close()
1460
1461def test_request_extra_json(self, client: OpenAI) -> None:
1462request = client._build_request(
08b8179aDavid Schnurr2 years ago1463FinalRequestOptions(
1464method="post",
1465url="/foo",
1466json_data={"foo": "bar"},
1467extra_json={"baz": False},
1468),
1469)
1470data = json.loads(request.content.decode("utf-8"))
1471assert data == {"foo": "bar", "baz": False}
1472
53278443stainless-app[bot]8 months ago1473request = client._build_request(
08b8179aDavid Schnurr2 years ago1474FinalRequestOptions(
1475method="post",
1476url="/foo",
1477extra_json={"baz": False},
1478),
1479)
1480data = json.loads(request.content.decode("utf-8"))
1481assert data == {"baz": False}
1482
1483# `extra_json` takes priority over `json_data` when keys clash
53278443stainless-app[bot]8 months ago1484request = client._build_request(
08b8179aDavid Schnurr2 years ago1485FinalRequestOptions(
1486method="post",
1487url="/foo",
1488json_data={"foo": "bar", "baz": True},
1489extra_json={"baz": None},
1490),
1491)
1492data = json.loads(request.content.decode("utf-8"))
1493assert data == {"foo": "bar", "baz": None}
1494
53278443stainless-app[bot]8 months ago1495def test_request_extra_headers(self, client: OpenAI) -> None:
1496request = client._build_request(
08b8179aDavid Schnurr2 years ago1497FinalRequestOptions(
1498method="post",
1499url="/foo",
1500**make_request_options(extra_headers={"X-Foo": "Foo"}),
1501),
1502)
1503assert request.headers.get("X-Foo") == "Foo"
1504
1505# `extra_headers` takes priority over `default_headers` when keys clash
53278443stainless-app[bot]8 months ago1506request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
08b8179aDavid Schnurr2 years ago1507FinalRequestOptions(
1508method="post",
1509url="/foo",
1510**make_request_options(
1511extra_headers={"X-Bar": "false"},
1512),
1513),
1514)
1515assert request.headers.get("X-Bar") == "false"
1516
53278443stainless-app[bot]8 months ago1517def test_request_extra_query(self, client: OpenAI) -> None:
1518request = client._build_request(
08b8179aDavid Schnurr2 years ago1519FinalRequestOptions(
1520method="post",
1521url="/foo",
1522**make_request_options(
1523extra_query={"my_query_param": "Foo"},
1524),
1525),
1526)
31573844Stainless Bot2 years ago1527params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1528assert params == {"my_query_param": "Foo"}
1529
1530# if both `query` and `extra_query` are given, they are merged
53278443stainless-app[bot]8 months ago1531request = client._build_request(
08b8179aDavid Schnurr2 years ago1532FinalRequestOptions(
1533method="post",
1534url="/foo",
1535**make_request_options(
1536query={"bar": "1"},
1537extra_query={"foo": "2"},
1538),
1539),
1540)
31573844Stainless Bot2 years ago1541params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1542assert params == {"bar": "1", "foo": "2"}
1543
1544# `extra_query` takes priority over `query` when keys clash
53278443stainless-app[bot]8 months ago1545request = client._build_request(
08b8179aDavid Schnurr2 years ago1546FinalRequestOptions(
1547method="post",
1548url="/foo",
1549**make_request_options(
1550query={"foo": "1"},
1551extra_query={"foo": "2"},
1552),
1553),
1554)
31573844Stainless Bot2 years ago1555params = dict(request.url.params)
08b8179aDavid Schnurr2 years ago1556assert params == {"foo": "2"}
1557
22713fd0Stainless Bot2 years ago1558def test_multipart_repeating_array(self, async_client: AsyncOpenAI) -> None:
1559request = async_client._build_request(
1560FinalRequestOptions.construct(
fcbb5983stainless-app[bot]11 months ago1561method="post",
22713fd0Stainless Bot2 years ago1562url="/foo",
1563headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
1564json_data={"array": ["foo", "bar"]},
1565files=[("foo.txt", b"hello world")],
1566)
1567)
1568
1569assert request.read().split(b"\r\n") == [
1570b"--6b7ba517decee4a450543ea6ae821c82",
1571b'Content-Disposition: form-data; name="array[]"',
1572b"",
1573b"foo",
1574b"--6b7ba517decee4a450543ea6ae821c82",
1575b'Content-Disposition: form-data; name="array[]"',
1576b"",
1577b"bar",
1578b"--6b7ba517decee4a450543ea6ae821c82",
1579b'Content-Disposition: form-data; name="foo.txt"; filename="upload"',
1580b"Content-Type: application/octet-stream",
1581b"",
1582b"hello world",
1583b"--6b7ba517decee4a450543ea6ae821c82--",
1584b"",
1585]
1586
a532f6efstainless-app[bot]5 months ago1587@pytest.mark.respx(base_url=base_url)
1588async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
1589respx_mock.post("/upload").mock(side_effect=mirror_request_content)
1590
1591file_content = b"Hello, this is a test file."
1592
1593response = await async_client.post(
1594"/upload",
1595content=file_content,
1596cast_to=httpx.Response,
1597options={"headers": {"Content-Type": "application/octet-stream"}},
1598)
1599
1600assert response.status_code == 200
1601assert response.request.headers["Content-Type"] == "application/octet-stream"
1602assert response.content == file_content
1603
1604async def test_binary_content_upload_with_asynciterator(self) -> None:
1605file_content = b"Hello, this is a test file."
1606counter = Counter()
1607iterator = _make_async_iterator([file_content], counter=counter)
1608
1609async def mock_handler(request: httpx.Request) -> httpx.Response:
1610assert counter.value == 0, "the request body should not have been read"
1611return httpx.Response(200, content=await request.aread())
1612
1613async with AsyncOpenAI(
1614base_url=base_url,
1615api_key=api_key,
1616_strict_response_validation=True,
1617http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)),
1618) as client:
1619response = await client.post(
1620"/upload",
1621content=iterator,
1622cast_to=httpx.Response,
1623options={"headers": {"Content-Type": "application/octet-stream"}},
1624)
1625
1626assert response.status_code == 200
1627assert response.request.headers["Content-Type"] == "application/octet-stream"
1628assert response.content == file_content
1629assert counter.value == 1
1630
1631@pytest.mark.respx(base_url=base_url)
1632async def test_binary_content_upload_with_body_is_deprecated(
1633self, respx_mock: MockRouter, async_client: AsyncOpenAI
1634) -> None:
1635respx_mock.post("/upload").mock(side_effect=mirror_request_content)
1636
1637file_content = b"Hello, this is a test file."
1638
1639with pytest.deprecated_call(
1640match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
1641):
1642response = await async_client.post(
1643"/upload",
1644body=file_content,
1645cast_to=httpx.Response,
1646options={"headers": {"Content-Type": "application/octet-stream"}},
1647)
1648
1649assert response.status_code == 200
1650assert response.request.headers["Content-Type"] == "application/octet-stream"
1651assert response.content == file_content
1652
08b8179aDavid Schnurr2 years ago1653@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1654async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1655class Model1(BaseModel):
1656name: str
1657
1658class Model2(BaseModel):
1659foo: str
1660
1661respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1662
53278443stainless-app[bot]8 months ago1663response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago1664assert isinstance(response, Model2)
1665assert response.foo == "bar"
1666
1667@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1668async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1669"""Union of objects with the same field name using a different type"""
1670
1671class Model1(BaseModel):
1672foo: int
1673
1674class Model2(BaseModel):
1675foo: str
1676
1677respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1678
53278443stainless-app[bot]8 months ago1679response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago1680assert isinstance(response, Model2)
1681assert response.foo == "bar"
1682
1683respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
1684
53278443stainless-app[bot]8 months ago1685response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
08b8179aDavid Schnurr2 years ago1686assert isinstance(response, Model1)
1687assert response.foo == 1
1688
c26014e2Stainless Bot2 years ago1689@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1690async def test_non_application_json_content_type_for_json_data(
1691self, respx_mock: MockRouter, async_client: AsyncOpenAI
1692) -> None:
c26014e2Stainless Bot2 years ago1693"""
1694Response that sets Content-Type to something other than application/json but returns json data
1695"""
1696
1697class Model(BaseModel):
1698foo: int
1699
1700respx_mock.get("/foo").mock(
1701return_value=httpx.Response(
1702200,
1703content=json.dumps({"foo": 2}),
1704headers={"Content-Type": "application/text"},
1705)
1706)
1707
53278443stainless-app[bot]8 months ago1708response = await async_client.get("/foo", cast_to=Model)
c26014e2Stainless Bot2 years ago1709assert isinstance(response, Model)
1710assert response.foo == 2
1711
53278443stainless-app[bot]8 months ago1712async def test_base_url_setter(self) -> None:
f6f38a9bStainless Bot2 years ago1713client = AsyncOpenAI(
1714base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
1715)
1716assert client.base_url == "https://example.com/from_init/"
1717
1718client.base_url = "https://example.com/from_setter" # type: ignore[assignment]
1719
1720assert client.base_url == "https://example.com/from_setter/"
1721
53278443stainless-app[bot]8 months ago1722await client.close()
1723
1724async def test_base_url_env(self) -> None:
0733934fStainless Bot2 years ago1725with update_env(OPENAI_BASE_URL="http://localhost:5000/from/env"):
1726client = AsyncOpenAI(api_key=api_key, _strict_response_validation=True)
1727assert client.base_url == "http://localhost:5000/from/env/"
1728
08b8179aDavid Schnurr2 years ago1729@pytest.mark.parametrize(
1730"client",
1731[
1732AsyncOpenAI(
1733base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1734),
1735AsyncOpenAI(
1736base_url="http://localhost:5000/custom/path/",
1737api_key=api_key,
1738_strict_response_validation=True,
1739http_client=httpx.AsyncClient(),
1740),
1741],
1742ids=["standard", "custom http client"],
1743)
53278443stainless-app[bot]8 months ago1744async def test_base_url_trailing_slash(self, client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1745request = client._build_request(
1746FinalRequestOptions(
1747method="post",
1748url="/foo",
1749json_data={"foo": "bar"},
1750),
1751)
1752assert request.url == "http://localhost:5000/custom/path/foo"
53278443stainless-app[bot]8 months ago1753await client.close()
08b8179aDavid Schnurr2 years ago1754
1755@pytest.mark.parametrize(
1756"client",
1757[
1758AsyncOpenAI(
1759base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1760),
1761AsyncOpenAI(
1762base_url="http://localhost:5000/custom/path/",
1763api_key=api_key,
1764_strict_response_validation=True,
1765http_client=httpx.AsyncClient(),
1766),
1767],
1768ids=["standard", "custom http client"],
1769)
53278443stainless-app[bot]8 months ago1770async def test_base_url_no_trailing_slash(self, client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1771request = client._build_request(
1772FinalRequestOptions(
1773method="post",
1774url="/foo",
1775json_data={"foo": "bar"},
1776),
1777)
1778assert request.url == "http://localhost:5000/custom/path/foo"
53278443stainless-app[bot]8 months ago1779await client.close()
08b8179aDavid Schnurr2 years ago1780
1781@pytest.mark.parametrize(
1782"client",
1783[
1784AsyncOpenAI(
1785base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True
1786),
1787AsyncOpenAI(
1788base_url="http://localhost:5000/custom/path/",
1789api_key=api_key,
1790_strict_response_validation=True,
1791http_client=httpx.AsyncClient(),
1792),
1793],
1794ids=["standard", "custom http client"],
1795)
53278443stainless-app[bot]8 months ago1796async def test_absolute_request_url(self, client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1797request = client._build_request(
1798FinalRequestOptions(
1799method="post",
1800url="https://myapi.com/foo",
1801json_data={"foo": "bar"},
1802),
1803)
1804assert request.url == "https://myapi.com/foo"
53278443stainless-app[bot]8 months ago1805await client.close()
08b8179aDavid Schnurr2 years ago1806
1807async def test_copied_client_does_not_close_http(self) -> None:
53278443stainless-app[bot]8 months ago1808test_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1809assert not test_client.is_closed()
08b8179aDavid Schnurr2 years ago1810
53278443stainless-app[bot]8 months ago1811copied = test_client.copy()
1812assert copied is not test_client
08b8179aDavid Schnurr2 years ago1813
a7ebc260Stainless Bot2 years ago1814del copied
08b8179aDavid Schnurr2 years ago1815
1816await asyncio.sleep(0.2)
53278443stainless-app[bot]8 months ago1817assert not test_client.is_closed()
08b8179aDavid Schnurr2 years ago1818
1819async def test_client_context_manager(self) -> None:
53278443stainless-app[bot]8 months ago1820test_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1821async with test_client as c2:
1822assert c2 is test_client
08b8179aDavid Schnurr2 years ago1823assert not c2.is_closed()
53278443stainless-app[bot]8 months ago1824assert not test_client.is_closed()
1825assert test_client.is_closed()
08b8179aDavid Schnurr2 years ago1826
1827@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1828async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1829class Model(BaseModel):
1830foo: str
1831
1832respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
1833
1834with pytest.raises(APIResponseValidationError) as exc:
53278443stainless-app[bot]8 months ago1835await async_client.get("/foo", cast_to=Model)
08b8179aDavid Schnurr2 years ago1836
1837assert isinstance(exc.value.__cause__, ValidationError)
1838
07079085Stainless Bot2 years ago1839async def test_client_max_retries_validation(self) -> None:
1840with pytest.raises(TypeError, match=r"max_retries cannot be None"):
1841AsyncOpenAI(
1842base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)
1843)
1844
08b8179aDavid Schnurr2 years ago1845@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago1846async def test_default_stream_cls(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
08b8179aDavid Schnurr2 years ago1847class Model(BaseModel):
1848name: str
1849
1850respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
1851
53278443stainless-app[bot]8 months ago1852stream = await async_client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
86379b44Stainless Bot2 years ago1853assert isinstance(stream, AsyncStream)
1854await stream.response.aclose()
08b8179aDavid Schnurr2 years ago1855
1856@pytest.mark.respx(base_url=base_url)
1857async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
1858class Model(BaseModel):
1859name: str
1860
1861respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
1862
1863strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
1864
1865with pytest.raises(APIResponseValidationError):
1866await strict_client.get("/foo", cast_to=Model)
1867
53278443stainless-app[bot]8 months ago1868non_strict_client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=False)
08b8179aDavid Schnurr2 years ago1869
53278443stainless-app[bot]8 months ago1870response = await non_strict_client.get("/foo", cast_to=Model)
08b8179aDavid Schnurr2 years ago1871assert isinstance(response, str) # type: ignore[unreachable]
1872
53278443stainless-app[bot]8 months ago1873await strict_client.close()
1874await non_strict_client.close()
1875
08b8179aDavid Schnurr2 years ago1876@pytest.mark.parametrize(
1877"remaining_retries,retry_after,timeout",
1878[
1879[3, "20", 20],
1880[3, "0", 0.5],
1881[3, "-10", 0.5],
1882[3, "60", 60],
1883[3, "61", 0.5],
1884[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
1885[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
1886[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
1887[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
1888[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
1889[3, "99999999999999999999999999999999999", 0.5],
1890[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
1891[3, "", 0.5],
1892[2, "", 0.5 * 2.0],
1893[1, "", 0.5 * 4.0],
7f6a921cstainless-app[bot]1 years ago1894[-1100, "", 8], # test large number potentially overflowing
08b8179aDavid Schnurr2 years ago1895],
1896)
1897@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
53278443stainless-app[bot]8 months ago1898async def test_parse_retry_after_header(
1899self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncOpenAI
1900) -> None:
08b8179aDavid Schnurr2 years ago1901headers = httpx.Headers({"retry-after": retry_after})
1902options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
53278443stainless-app[bot]8 months ago1903calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
08b8179aDavid Schnurr2 years ago1904assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
7aad3405Stainless Bot2 years ago1905
ba4f7a97Stainless Bot2 years ago1906@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1907@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1908async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1909respx_mock.post("/chat/completions").mock(side_effect=httpx.TimeoutException("Test timeout error"))
1910
1911with pytest.raises(APITimeoutError):
0bef1d02stainless-app[bot]1 years ago1912await async_client.chat.completions.with_streaming_response.create(
1913messages=[
1914{
1915"content": "string",
1916"role": "developer",
1917}
1918],
1919model="gpt-4o",
1920).__aenter__()
7aad3405Stainless Bot2 years ago1921
53278443stainless-app[bot]8 months ago1922assert _get_open_connections(async_client) == 0
7aad3405Stainless Bot2 years ago1923
ba4f7a97Stainless Bot2 years ago1924@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
7aad3405Stainless Bot2 years ago1925@pytest.mark.respx(base_url=base_url)
0bef1d02stainless-app[bot]1 years ago1926async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
ba4f7a97Stainless Bot2 years ago1927respx_mock.post("/chat/completions").mock(return_value=httpx.Response(500))
7aad3405Stainless Bot2 years ago1928
ba4f7a97Stainless Bot2 years ago1929with pytest.raises(APIStatusError):
0bef1d02stainless-app[bot]1 years ago1930await async_client.chat.completions.with_streaming_response.create(
1931messages=[
1932{
1933"content": "string",
1934"role": "developer",
1935}
1936],
1937model="gpt-4o",
1938).__aenter__()
53278443stainless-app[bot]8 months ago1939assert _get_open_connections(async_client) == 0
98d8b2acstainless-app[bot]1 years ago1940
1941@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1942@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1943@pytest.mark.respx(base_url=base_url)
6634f525stainless-app[bot]1 years ago1944@pytest.mark.parametrize("failure_mode", ["status", "exception"])
98d8b2acstainless-app[bot]1 years ago1945async def test_retries_taken(
6634f525stainless-app[bot]1 years ago1946self,
1947async_client: AsyncOpenAI,
1948failures_before_success: int,
1949failure_mode: Literal["status", "exception"],
1950respx_mock: MockRouter,
98d8b2acstainless-app[bot]1 years ago1951) -> None:
1952client = async_client.with_options(max_retries=4)
1953
1954nb_retries = 0
1955
1956def retry_handler(_request: httpx.Request) -> httpx.Response:
1957nonlocal nb_retries
1958if nb_retries < failures_before_success:
1959nb_retries += 1
6634f525stainless-app[bot]1 years ago1960if failure_mode == "exception":
1961raise RuntimeError("oops")
98d8b2acstainless-app[bot]1 years ago1962return httpx.Response(500)
1963return httpx.Response(200)
1964
1965respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1966
1967response = await client.chat.completions.with_raw_response.create(
1968messages=[
1969{
bf1ca86cRobert Craigie1 years ago1970"content": "string",
575ff607stainless-app[bot]1 years ago1971"role": "developer",
98d8b2acstainless-app[bot]1 years ago1972}
1973],
bf1ca86cRobert Craigie1 years ago1974model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago1975)
1976
1977assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago1978assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
1979
1980@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1981@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1982@pytest.mark.respx(base_url=base_url)
1983async def test_omit_retry_count_header(
1984self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
1985) -> None:
1986client = async_client.with_options(max_retries=4)
1987
1988nb_retries = 0
1989
1990def retry_handler(_request: httpx.Request) -> httpx.Response:
1991nonlocal nb_retries
1992if nb_retries < failures_before_success:
1993nb_retries += 1
1994return httpx.Response(500)
1995return httpx.Response(200)
1996
1997respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
1998
1999response = await client.chat.completions.with_raw_response.create(
2000messages=[
2001{
2002"content": "string",
575ff607stainless-app[bot]1 years ago2003"role": "developer",
5449e208Stainless Bot1 years ago2004}
2005],
2006model="gpt-4o",
2007extra_headers={"x-stainless-retry-count": Omit()},
2008)
2009
2010assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
2011
2012@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
2013@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
2014@pytest.mark.respx(base_url=base_url)
2015async def test_overwrite_retry_count_header(
2016self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
2017) -> None:
2018client = async_client.with_options(max_retries=4)
2019
2020nb_retries = 0
2021
2022def retry_handler(_request: httpx.Request) -> httpx.Response:
2023nonlocal nb_retries
2024if nb_retries < failures_before_success:
2025nb_retries += 1
2026return httpx.Response(500)
2027return httpx.Response(200)
2028
2029respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
2030
2031response = await client.chat.completions.with_raw_response.create(
2032messages=[
2033{
2034"content": "string",
575ff607stainless-app[bot]1 years ago2035"role": "developer",
5449e208Stainless Bot1 years ago2036}
2037],
2038model="gpt-4o",
2039extra_headers={"x-stainless-retry-count": "42"},
2040)
2041
2042assert response.http_request.headers.get("x-stainless-retry-count") == "42"
98d8b2acstainless-app[bot]1 years ago2043
2044@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
2045@mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
2046@pytest.mark.respx(base_url=base_url)
2047async def test_retries_taken_new_response_class(
2048self, async_client: AsyncOpenAI, failures_before_success: int, respx_mock: MockRouter
2049) -> None:
2050client = async_client.with_options(max_retries=4)
2051
2052nb_retries = 0
2053
2054def retry_handler(_request: httpx.Request) -> httpx.Response:
2055nonlocal nb_retries
2056if nb_retries < failures_before_success:
2057nb_retries += 1
2058return httpx.Response(500)
2059return httpx.Response(200)
2060
2061respx_mock.post("/chat/completions").mock(side_effect=retry_handler)
2062
2063async with client.chat.completions.with_streaming_response.create(
2064messages=[
2065{
bf1ca86cRobert Craigie1 years ago2066"content": "string",
575ff607stainless-app[bot]1 years ago2067"role": "developer",
98d8b2acstainless-app[bot]1 years ago2068}
2069],
bf1ca86cRobert Craigie1 years ago2070model="gpt-4o",
98d8b2acstainless-app[bot]1 years ago2071) as response:
2072assert response.retries_taken == failures_before_success
5449e208Stainless Bot1 years ago2073assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
d8901d28Seth Gilchrist1 years ago2074
4756247cstainless-app[bot]9 months ago2075async def test_get_platform(self) -> None:
2076platform = await asyncify(get_platform)()
2077assert isinstance(platform, (str, OtherPlatform))
cca09707stainless-app[bot]1 years ago2078
cc2c1fc1stainless-app[bot]1 years ago2079async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
2080# Test that the proxy environment variables are set correctly
2081monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
2082
2083client = DefaultAsyncHttpxClient()
2084
2085mounts = tuple(client._mounts.items())
2086assert len(mounts) == 1
2087assert mounts[0][0].pattern == "https://"
2088
2089@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
2090async def test_default_client_creation(self) -> None:
2091# Ensure that the client can be initialized without any exceptions
2092DefaultAsyncHttpxClient(
2093verify=True,
2094cert=None,
2095trust_env=True,
2096http1=True,
2097http2=False,
2098limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
2099)
2100
cca09707stainless-app[bot]1 years ago2101@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago2102async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
cca09707stainless-app[bot]1 years ago2103# Test that the default follow_redirects=True allows following redirects
2104respx_mock.post("/redirect").mock(
2105return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
2106)
2107respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
2108
53278443stainless-app[bot]8 months ago2109response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
cca09707stainless-app[bot]1 years ago2110assert response.status_code == 200
2111assert response.json() == {"status": "ok"}
2112
2113@pytest.mark.respx(base_url=base_url)
53278443stainless-app[bot]8 months ago2114async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncOpenAI) -> None:
cca09707stainless-app[bot]1 years ago2115# Test that follow_redirects=False prevents following redirects
2116respx_mock.post("/redirect").mock(
2117return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
2118)
2119
2120with pytest.raises(APIStatusError) as exc_info:
53278443stainless-app[bot]8 months ago2121await async_client.post(
cca09707stainless-app[bot]1 years ago2122"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
2123)
2124
2125assert exc_info.value.response.status_code == 302
2126assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
25d16be1Johan Stenberg (MSFT)9 months ago2127
2128@pytest.mark.asyncio
2129async def test_api_key_before_after_refresh_provider(self) -> None:
2130async def mock_api_key_provider():
2131return "test_bearer_token"
2132
2133client = AsyncOpenAI(base_url=base_url, api_key=mock_api_key_provider)
2134
2135assert client.api_key == ""
2136assert "Authorization" not in client.auth_headers
2137
2138await client._refresh_api_key()
2139
2140assert client.api_key == "test_bearer_token"
2141assert client.auth_headers.get("Authorization") == "Bearer test_bearer_token"
2142
2143@pytest.mark.asyncio
2144async def test_api_key_before_after_refresh_str(self) -> None:
2145client = AsyncOpenAI(base_url=base_url, api_key="test_api_key")
2146
2147assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
2148await client._refresh_api_key()
2149
2150assert client.auth_headers.get("Authorization") == "Bearer test_api_key"
2151
2152@pytest.mark.asyncio
2153@pytest.mark.respx()
2154async def test_bearer_token_refresh_async(self, respx_mock: MockRouter) -> None:
2155respx_mock.post(base_url + "/chat/completions").mock(
2156side_effect=[
2157httpx.Response(500, json={"error": "server error"}),
2158httpx.Response(200, json={"foo": "bar"}),
2159]
2160)
2161
2162counter = 0
2163
2164async def token_provider() -> str:
2165nonlocal counter
2166
2167counter += 1
2168
2169if counter == 1:
2170return "first"
2171
2172return "second"
2173
2174client = AsyncOpenAI(base_url=base_url, api_key=token_provider)
2175await client.chat.completions.create(messages=[], model="gpt-4")
2176
2177calls = cast("list[MockRequestCall]", respx_mock.calls)
2178assert len(calls) == 2
2179
2180assert calls[0].request.headers.get("Authorization") == "Bearer first"
2181assert calls[1].request.headers.get("Authorization") == "Bearer second"
2182
2183@pytest.mark.asyncio
2184async def test_copy_auth(self) -> None:
2185async def token_provider_1() -> str:
2186return "test_bearer_token_1"
2187
2188async def token_provider_2() -> str:
2189return "test_bearer_token_2"
2190
2191client = AsyncOpenAI(base_url=base_url, api_key=token_provider_1).copy(api_key=token_provider_2)
2192await client._refresh_api_key()
2193assert client.auth_headers == {"Authorization": "Bearer test_bearer_token_2"}