From 0aacbb515502a36570b0f078e3a4f548df5d3cf5 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 10 Sep 2026 03:18:13 +0100 Subject: [PATCH 1/4] fix(querystring): preserve empty string values --- src/openai/_qs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/openai/_qs.py b/src/openai/_qs.py index 4127c19c62..d3c34ce6b0 100644 --- a/src/openai/_qs.py +++ b/src/openai/_qs.py @@ -112,10 +112,9 @@ def _stringify_item( f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" ) - serialised = self._primitive_value_to_str(value) - if not serialised: + if value is None: return [] - return [(key, serialised)] + return [(key, self._primitive_value_to_str(value))] def _primitive_value_to_str(self, value: PrimitiveData) -> str: # copied from httpx From 11607aa53993632a7a7f868ea08d6496bfde0ab3 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 10 Sep 2026 03:18:28 +0100 Subject: [PATCH 2/4] test(querystring): cover empty string values --- tests/test_qs_empty_values.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/test_qs_empty_values.py diff --git a/tests/test_qs_empty_values.py b/tests/test_qs_empty_values.py new file mode 100644 index 0000000000..658b74cc7c --- /dev/null +++ b/tests/test_qs_empty_values.py @@ -0,0 +1,19 @@ +from urllib.parse import unquote + +from openai._qs import stringify + + +def test_empty_string_scalar_is_preserved() -> None: + assert stringify({"filter": ""}) == "filter=" + + +def test_none_scalar_is_still_omitted() -> None: + assert stringify({"filter": None}) == "" + + +def test_nested_empty_string_is_preserved() -> None: + assert unquote(stringify({"filter": {"name": ""}})) == "filter[name]=" + + +def test_empty_string_in_repeat_array_is_preserved() -> None: + assert stringify({"filter": ["", "active"]}) == "filter=&filter=active" From 2eff92f005d9df6dd88ec75b864d4af9c336ae4a Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:09:20 +0100 Subject: [PATCH 3/4] Omit all-None comma query arrays --- src/openai/_qs.py | 5 ++++- tests/test_qs.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/openai/_qs.py b/src/openai/_qs.py index d3c34ce6b0..8dc8b6797e 100644 --- a/src/openai/_qs.py +++ b/src/openai/_qs.py @@ -85,10 +85,13 @@ def _stringify_item( if isinstance(value, (list, tuple)): array_format = opts.array_format if array_format == "comma": + values = [item for item in value if item is not None] + if not values: + return [] return [ ( key, - ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ",".join(self._primitive_value_to_str(item) for item in values), ), ] elif array_format == "repeat": diff --git a/tests/test_qs.py b/tests/test_qs.py index 697b8a95ec..1bcfbadedd 100644 --- a/tests/test_qs.py +++ b/tests/test_qs.py @@ -52,6 +52,10 @@ def test_array_comma(method: str) -> None: assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" + assert unquote(serialise({"filter": []})) == "" + assert unquote(serialise({"filter": [None]})) == "" + assert unquote(serialise({"filter": [""]})) == "filter=" + assert unquote(serialise({"filter": [None, "active"]})) == "filter=active" def test_array_repeat() -> None: From d42e99f921b1fb654c64a0fa8df3ff6d9f517e84 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:48:43 +0100 Subject: [PATCH 4/4] Test empty query arrays across formats --- tests/test_qs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_qs.py b/tests/test_qs.py index 1bcfbadedd..722c98c898 100644 --- a/tests/test_qs.py +++ b/tests/test_qs.py @@ -5,6 +5,7 @@ import pytest from openai._qs import Querystring, stringify +from openai._types import ArrayFormat def test_empty() -> None: @@ -58,6 +59,13 @@ def test_array_comma(method: str) -> None: assert unquote(serialise({"filter": [None, "active"]})) == "filter=active" +@pytest.mark.parametrize("array_format", ["comma", "repeat", "indices", "brackets"]) +def test_empty_array_omits_parameter(array_format: ArrayFormat) -> None: + # Non-empty output remains format-specific; only the empty outcome is shared. + assert stringify({"filter": []}, array_format=array_format) == "" + assert stringify({"filter": [None]}, array_format=array_format) == "" + + def test_array_repeat() -> None: assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false"