diff --git a/README.md b/README.md index 21f79312a1..5aa697b554 100644 --- a/README.md +++ b/README.md @@ -678,6 +678,12 @@ print(completion) These methods return a [`LegacyAPIResponse`](https://github.com/openai/openai-python/tree/main/src/openai/_legacy_response.py) object. This is a legacy class as we're changing it slightly in the next major version. +Tag your own requests for easier support follow-up: + +```py +client.responses.create(..., extra_headers={"X-Client-Request-Id": "123e4567-e89b-12d3-a456-426614174000"}) +``` + For the sync client this will mostly be the same with the exception of `content` & `text` will be methods instead of properties. In the async client, all methods will be async. diff --git a/pyproject.toml b/pyproject.toml index a4850a0f49..b783f565a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai" -version = "2.7.1" +version = "2.7.2" description = "The official Python library for the openai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index 58490e4430..7404c7d53d 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -438,6 +438,22 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 # headers are case-insensitive while dictionaries are not. headers = httpx.Headers(headers_dict) + option_client_request_id = options.client_request_id + if option_client_request_id: + headers["X-Client-Request-Id"] = option_client_request_id + else: + client_request_id_header = None + for key, value in custom_headers.items(): + if key.lower() != "x-client-request-id": + continue + if isinstance(value, Omit) or value in (None, ""): + break + client_request_id_header = str(value) + break + + if client_request_id_header: + headers["X-Client-Request-Id"] = client_request_id_header + idempotency_header = self._idempotency_header if idempotency_header and options.idempotency_key and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key @@ -1850,6 +1866,7 @@ def make_request_options( extra_query: Query | None = None, extra_body: Body | None = None, idempotency_key: str | None = None, + client_request_id: str | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, post_parser: PostParser | NotGiven = not_given, ) -> RequestOptions: @@ -1873,6 +1890,9 @@ def make_request_options( if idempotency_key is not None: options["idempotency_key"] = idempotency_key + if client_request_id: + options["client_request_id"] = client_request_id + if is_given(post_parser): # internal options["post_parser"] = post_parser # type: ignore diff --git a/src/openai/_models.py b/src/openai/_models.py index af71a91850..1cd641679d 100644 --- a/src/openai/_models.py +++ b/src/openai/_models.py @@ -805,6 +805,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + client_request_id: str | None json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -820,6 +821,7 @@ class FinalRequestOptions(pydantic.BaseModel): timeout: Union[float, Timeout, None, NotGiven] = NotGiven() files: Union[HttpxRequestFiles, None] = None idempotency_key: Union[str, None] = None + client_request_id: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None diff --git a/src/openai/_types.py b/src/openai/_types.py index 2387d7e01c..4ae06ceac0 100644 --- a/src/openai/_types.py +++ b/src/openai/_types.py @@ -112,6 +112,7 @@ class RequestOptions(TypedDict, total=False): params: Query extra_json: AnyMapping idempotency_key: str + client_request_id: str | None follow_redirects: bool diff --git a/src/openai/_version.py b/src/openai/_version.py index 9fb4c23dba..6c0fcb3469 100644 --- a/src/openai/_version.py +++ b/src/openai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "openai" -__version__ = "2.7.1" # x-release-please-version +__version__ = "2.7.2" # x-release-please-version diff --git a/tests/test_client.py b/tests/test_client.py index e8d62f17f7..5e7cf5c316 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -349,6 +349,29 @@ def test_default_headers_option(self) -> None: test_client.close() test_client2.close() + def test_client_request_id_header_from_option(self, client: OpenAI) -> None: + request = client._build_request( + FinalRequestOptions(method="get", url="/foo", client_request_id="custom-option-id") + ) + + assert request.headers.get("X-Client-Request-Id") == "custom-option-id" + + def test_client_request_id_header_from_custom_headers(self, client: OpenAI) -> None: + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + headers={"x-client-request-id": "custom-header-id"}, + ) + ) + + assert request.headers.get("X-Client-Request-Id") == "custom-header-id" + + def test_client_request_id_header_absent_when_unspecified(self, client: OpenAI) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + + assert request.headers.get("X-Client-Request-Id") is None + def test_validate_headers(self) -> None: client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) options = client._prepare_options(FinalRequestOptions(method="get", url="/foo"))