diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4416b1e547..a0d583cc18 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,8 @@ - - - - - -- [ ] I understand that this repository is auto-generated and my pull request may not be merged + + + + + ## Changes being requested diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e71fffe08..57ed4c4fd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,22 @@ +# Contributing + +## Contribution policy + +We welcome bug reports, feature requests, minimal reproductions, and root-cause +analysis through [GitHub issues](https://github.com/openai/openai-python/issues). + +**Pull requests are limited to repository collaborators. We do not accept pull +requests from non-collaborators**, including documentation or example changes. +If you are not a collaborator, please open an issue instead of preparing a pull +request. Include the affected version, expected and actual behavior, and a small, +sanitized reproduction when applicable. + +Report suspected security vulnerabilities privately as described in +[SECURITY.md](SECURITY.md), rather than in issues or pull requests. + +The development and pull request instructions below are for maintainers and +repository collaborators. + ## Setting up the environment The minimum supported runtime, contributor toolchain, CI matrix, and release diff --git a/README.md b/README.md index d278292354..c1001e7733 100644 --- a/README.md +++ b/README.md @@ -1249,4 +1249,7 @@ Python 3.10 or higher. ## Contributing -See [the contributing documentation](./CONTRIBUTING.md). +Please share bug reports and feature requests through [GitHub issues](https://github.com/openai/openai-python/issues). +Pull requests are limited to repository collaborators; we do not accept pull requests from non-collaborators. +See [CONTRIBUTING.md](https://github.com/openai/openai-python/blob/main/CONTRIBUTING.md) for the contribution policy and development guide. +For security vulnerabilities, follow [SECURITY.md](https://github.com/openai/openai-python/blob/main/SECURITY.md). diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index be99ab7683..c8830a6a9f 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -530,7 +530,7 @@ def _custom_auth( def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx2.Headers: custom_headers = options.headers or {} - headers_dict = _merge_mappings({**self._auth_headers(options.security), **self.default_headers}, custom_headers) + headers_dict = _merge_headers(self._auth_headers(options.security), self.default_headers, custom_headers) self._validate_headers(headers_dict, custom_headers) # headers are case-insensitive while dictionaries are not. @@ -2342,3 +2342,17 @@ def _merge_mappings( """ merged = {**obj1, **obj2} return {key: value for key, value in merged.items() if not isinstance(value, Omit)} + + +def _merge_headers(*mappings: Headers) -> dict[str, str]: + """Merge headers case-insensitively, with later mappings taking precedence.""" + merged: dict[str, tuple[str, str]] = {} + for mapping in mappings: + for name, value in mapping.items(): + normalized_name = name.lower() + if isinstance(value, Omit): + merged.pop(normalized_name, None) + else: + merged[normalized_name] = (name, value) + + return {name: value for name, value in merged.values()} diff --git a/tests/test_client.py b/tests/test_client.py index 01626384b4..6e8b4d2e72 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -156,6 +156,40 @@ def _get_open_connections(client: OpenAI | AsyncOpenAI) -> int: return len(cast(Any, transport)._pool._requests) +@pytest.mark.parametrize("is_async", [False, True]) +@pytest.mark.parametrize( + "extra_headers,expected", + [ + ({}, ["Bearer fake-default"]), + ({"AUTHORIZATION": "Bearer fake-request"}, ["Bearer fake-request"]), + ({"AUTHORIZATION": Omit()}, []), + ], + ids=["default", "override", "omit"], +) +async def test_case_insensitive_auth_headers( + is_async: bool, extra_headers: dict[str, str | Omit], expected: list[str] +) -> None: + def handler(request: httpx2.Request) -> httpx2.Response: + assert request.headers.get_list("authorization") == expected + return httpx2.Response(200, json={"object": "list", "data": []}) + + transport = httpx2.MockTransport(handler) + if is_async: + async with AsyncOpenAI( + api_key="fake-original", + default_headers={"authorization": "Bearer fake-default"}, + http_client=httpx2.AsyncClient(transport=transport), + ) as async_client: + await async_client.models.list(extra_headers=extra_headers) + else: + with OpenAI( + api_key="fake-original", + default_headers={"authorization": "Bearer fake-default"}, + http_client=httpx2.Client(transport=transport), + ) as client: + client.models.list(extra_headers=extra_headers) + + class TestOpenAI: @pytest.mark.parametrize( "code_fields,expected_code",