From 0140d6e4e7ed69faeed9a490a5b8a07101d06813 Mon Sep 17 00:00:00 2001 From: vrushankportkey Date: Tue, 11 Nov 2025 13:41:31 +0530 Subject: [PATCH 1/5] fix json serializing for openai agents repo --- examples/notgiven_serialization_example.py | 174 +++++++++++++++++++++ portkey_ai/__init__.py | 11 ++ portkey_ai/_vendor/openai/_types.py | 12 ++ portkey_ai/utils/json_utils.py | 50 +++++- tests/test_notgiven_serialization.py | 121 ++++++++++++++ 5 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 examples/notgiven_serialization_example.py create mode 100644 tests/test_notgiven_serialization.py diff --git a/examples/notgiven_serialization_example.py b/examples/notgiven_serialization_example.py new file mode 100644 index 00000000..cd22f29e --- /dev/null +++ b/examples/notgiven_serialization_example.py @@ -0,0 +1,174 @@ +""" +Example: Using Portkey with openai-agents (NotGiven Serialization) + +This example demonstrates that Portkey now automatically handles NotGiven +serialization out of the box. No manual setup required! + +The fix is transparent - just import and use Portkey normally. +""" + +import portkey_ai +from portkey_ai import AsyncPortkey, enable_notgiven_serialization, disable_notgiven_serialization +import json + + +def example_automatic_serialization(): + """Example showing automatic serialization works out of the box.""" + print("=== Automatic Serialization (No Setup Needed!) ===\n") + + from portkey_ai._vendor.openai._types import NOT_GIVEN + + # Data that contains NotGiven objects + data = { + "model": "gpt-4", + "temperature": 0.7, + "optional_param": NOT_GIVEN, # This works automatically now! + "another_param": None, + } + + # Use standard json.dumps - no special encoder needed! + try: + json_string = json.dumps(data) # Just works! ✨ + print(f"✅ Serialization works automatically: {json_string}\n") + except TypeError as e: + print(f"❌ Serialization failed: {e}\n") + + +def example_basic_usage(): + """Basic example of using PortkeyJSONEncoder for manual serialization.""" + print("=== Custom Encoder (Optional) ===\n") + + from portkey_ai import PortkeyJSONEncoder + from portkey_ai._vendor.openai._types import NOT_GIVEN + + # You can still use the custom encoder if you prefer + data = { + "model": "gpt-4", + "temperature": 0.7, + "optional_param": NOT_GIVEN, + "another_param": None, + } + + # Serialize using PortkeyJSONEncoder + try: + json_string = json.dumps(data, cls=PortkeyJSONEncoder) + print(f"✅ Successfully serialized with encoder: {json_string}\n") + except TypeError as e: + print(f"❌ Serialization failed: {e}\n") + + +def example_global_serialization(): + """Example showing disable/enable functionality.""" + print("=== Manual Enable/Disable Example ===\n") + + from portkey_ai._vendor.openai._types import NOT_GIVEN + + print("Note: Serialization is already enabled automatically!") + print("But you can disable and re-enable if needed:\n") + + # Disable temporarily + print("1. Disabling serialization...") + disable_notgiven_serialization() + + data = {"param": NOT_GIVEN} + + try: + json.dumps(data) + print(" ✅ Still works (unexpected)") + except TypeError: + print(" ✅ Correctly disabled - can't serialize NotGiven") + + # Re-enable + print("\n2. Re-enabling serialization...") + enable_notgiven_serialization() + + try: + json_string = json.dumps(data) + print(f" ✅ Works again: {json_string}\n") + except TypeError as e: + print(f" ❌ Failed: {e}\n") + + +def example_with_portkey_client(): + """Example showing how to use with a Portkey client.""" + print("=== Portkey Client Example ===\n") + + print("Serialization is enabled automatically - no setup needed!") + + # Create Portkey client - serialization works out of the box! + client = AsyncPortkey( + api_key="your-portkey-api-key", # or set PORTKEY_API_KEY env var + virtual_key="your-virtual-key", # optional + ) + + print("✅ AsyncPortkey client created successfully") + print("✅ The client can be serialized by external libraries like openai-agents") + + # Example: Simulate what openai-agents might do + try: + # Try to serialize the client's attributes + client_dict = { + "api_key": client.api_key, + "base_url": str(client.base_url), + "virtual_key": client.virtual_key, + } + serialized = json.dumps(client_dict) + print(f"✅ Client attributes serialized successfully: {serialized}\n") + except TypeError as e: + print(f"❌ Client serialization failed: {e}\n") + + +def example_before_and_after(): + """Demonstrate the problem and solution side by side.""" + print("=== Before and After Comparison ===\n") + + from portkey_ai._vendor.openai._types import NOT_GIVEN + + data = {"param": NOT_GIVEN} + + # BEFORE: This would fail + print("Before enabling global serialization:") + try: + json.dumps(data) + print("✅ Serialization succeeded (unexpected)") + except TypeError as e: + print(f"❌ Serialization failed as expected: {str(e)[:50]}...") + + # AFTER: This works + print("\nAfter enabling global serialization:") + enable_notgiven_serialization() + try: + result = json.dumps(data) + print(f"✅ Serialization succeeded: {result}") + except TypeError as e: + print(f"❌ Serialization failed (unexpected): {e}") + + from portkey_ai import disable_notgiven_serialization + disable_notgiven_serialization() + print() + + +def main(): + """Run all examples.""" + print("\n" + "="*60) + print("Portkey NotGiven Serialization Examples") + print("="*60 + "\n") + + print("✨ Good News: Serialization works automatically!") + print(" No manual setup required - just import and use.\n") + + example_automatic_serialization() + example_with_portkey_client() + example_basic_usage() + example_global_serialization() + example_before_and_after() + + print("="*60) + print("\n✨ All examples completed!") + print("\n📚 Key Takeaway: NotGiven serialization works out of the box!") + print(" For more information, see docs/notgiven-serialization.md") + print("="*60 + "\n") + + +if __name__ == "__main__": + main() diff --git a/portkey_ai/__init__.py b/portkey_ai/__init__.py index 10479a8b..3a257a83 100644 --- a/portkey_ai/__init__.py +++ b/portkey_ai/__init__.py @@ -164,6 +164,14 @@ PORTKEY_PROXY_ENV, PORTKEY_GATEWAY_URL, ) +from portkey_ai.utils.json_utils import ( + PortkeyJSONEncoder, + enable_notgiven_serialization, + disable_notgiven_serialization, +) + +# Automatically enable NotGiven serialization. Users can call disable_notgiven_serialization() if needed. +enable_notgiven_serialization() api_key = os.environ.get(PORTKEY_API_KEY_ENV) base_url = os.environ.get(PORTKEY_PROXY_ENV, PORTKEY_BASE_URL) @@ -175,6 +183,9 @@ "LLMOptions", "Modes", "PortkeyResponse", + "PortkeyJSONEncoder", + "enable_notgiven_serialization", + "disable_notgiven_serialization", "ModesLiteral", "ProviderTypes", "ProviderTypesLiteral", diff --git a/portkey_ai/_vendor/openai/_types.py b/portkey_ai/_vendor/openai/_types.py index 2387d7e0..28642055 100644 --- a/portkey_ai/_vendor/openai/_types.py +++ b/portkey_ai/_vendor/openai/_types.py @@ -143,6 +143,18 @@ def __bool__(self) -> Literal[False]: def __repr__(self) -> str: return "NOT_GIVEN" + def __reduce__(self) -> tuple[type[NotGiven], tuple[()]]: + """Support for pickling/serialization.""" + return (self.__class__, ()) + + def __copy__(self) -> NotGiven: + """Return self since NotGiven is a singleton-like sentinel.""" + return self + + def __deepcopy__(self, memo: dict[int, Any]) -> NotGiven: + """Return self since NotGiven is a singleton-like sentinel.""" + return self + not_given = NotGiven() # for backwards compatibility: diff --git a/portkey_ai/utils/json_utils.py b/portkey_ai/utils/json_utils.py index 6b016caf..26821ee1 100644 --- a/portkey_ai/utils/json_utils.py +++ b/portkey_ai/utils/json_utils.py @@ -1,11 +1,53 @@ import json +from portkey_ai._vendor.openai._types import NotGiven + + +class PortkeyJSONEncoder(json.JSONEncoder): + """Custom JSON encoder that handles Portkey-specific types like NotGiven.""" + + def default(self, obj): + if isinstance(obj, NotGiven): + # Return None for NotGiven instances during JSON serialization + return None + return super().default(obj) + + +_original_json_encoder = None + + +def enable_notgiven_serialization(): + """ + Enable global JSON serialization support for NotGiven types. + """ + global _original_json_encoder + if _original_json_encoder is None: + _original_json_encoder = json.JSONEncoder.default + + def patched_default(self, obj): + if isinstance(obj, NotGiven): + return None + return _original_json_encoder(self, obj) + + json.JSONEncoder.default = patched_default + + +def disable_notgiven_serialization(): + """ + Disable global JSON serialization support for NotGiven types. + + This restores the original JSONEncoder behavior. + """ + global _original_json_encoder + if _original_json_encoder is not None: + json.JSONEncoder.default = _original_json_encoder + _original_json_encoder = None def serialize_kwargs(**kwargs): # Function to check if a value is serializable def is_serializable(value): try: - json.dumps(value) + json.dumps(value, cls=PortkeyJSONEncoder) return True except (TypeError, ValueError): return False @@ -14,14 +56,14 @@ def is_serializable(value): serializable_kwargs = {k: v for k, v in kwargs.items() if is_serializable(v)} # Convert to string representation - return json.dumps(serializable_kwargs) + return json.dumps(serializable_kwargs, cls=PortkeyJSONEncoder) def serialize_args(*args): # Function to check if a value is serializable def is_serializable(value): try: - json.dumps(value) + json.dumps(value, cls=PortkeyJSONEncoder) return True except (TypeError, ValueError): return False @@ -30,4 +72,4 @@ def is_serializable(value): serializable_args = [arg for arg in args if is_serializable(arg)] # Convert to string representation - return json.dumps(serializable_args) + return json.dumps(serializable_args, cls=PortkeyJSONEncoder) diff --git a/tests/test_notgiven_serialization.py b/tests/test_notgiven_serialization.py new file mode 100644 index 00000000..06e1e68e --- /dev/null +++ b/tests/test_notgiven_serialization.py @@ -0,0 +1,121 @@ +""" +Test NotGiven serialization functionality. + +This test verifies that NotGiven sentinel objects can be properly serialized +to JSON, which is necessary for compatibility with external libraries like +openai-agents that may attempt to serialize client objects for logging/tracing. +""" + +import json +import pytest +from portkey_ai._vendor.openai._types import NOT_GIVEN, NotGiven +from portkey_ai.utils.json_utils import ( + PortkeyJSONEncoder, + enable_notgiven_serialization, + disable_notgiven_serialization, +) + + +def test_notgiven_with_custom_encoder(): + """Test that PortkeyJSONEncoder can serialize NotGiven objects.""" + test_obj = { + "key1": "value1", + "key2": NOT_GIVEN, + "key3": 123, + "nested": { + "key4": NOT_GIVEN, + "key5": "value5" + } + } + + # Should not raise TypeError + result = json.dumps(test_obj, cls=PortkeyJSONEncoder) + parsed = json.loads(result) + + # NOT_GIVEN should be serialized as None + assert parsed["key1"] == "value1" + assert parsed["key2"] is None + assert parsed["key3"] == 123 + assert parsed["nested"]["key4"] is None + assert parsed["nested"]["key5"] == "value5" + + +def test_notgiven_in_list(): + """Test that NotGiven objects in lists are properly serialized.""" + test_list = [1, "test", NOT_GIVEN, {"key": NOT_GIVEN}] + + result = json.dumps(test_list, cls=PortkeyJSONEncoder) + parsed = json.loads(result) + + assert parsed[0] == 1 + assert parsed[1] == "test" + assert parsed[2] is None + assert parsed[3]["key"] is None + + +def test_enable_notgiven_serialization(): + """Test that enable_notgiven_serialization allows standard json.dumps to work.""" + # Note: Serialization is now enabled by default when portkey_ai is imported + # This test verifies it works correctly + + # It should work automatically (already enabled by module import) + result = json.dumps({"key": NOT_GIVEN}) + parsed = json.loads(result) + assert parsed["key"] is None + + # Test with nested structures + complex_obj = { + "a": NOT_GIVEN, + "b": [1, NOT_GIVEN, 3], + "c": {"nested": NOT_GIVEN} + } + result = json.dumps(complex_obj) + parsed = json.loads(result) + assert parsed["a"] is None + assert parsed["b"] == [1, None, 3] + assert parsed["c"]["nested"] is None + + # Test that we can disable and re-enable + disable_notgiven_serialization() + with pytest.raises(TypeError, match="not JSON serializable"): + json.dumps({"key": NOT_GIVEN}) + + # Re-enable + enable_notgiven_serialization() + result = json.dumps({"key": NOT_GIVEN}) + parsed = json.loads(result) + assert parsed["key"] is None + + +def test_disable_notgiven_serialization(): + """Test that disable_notgiven_serialization restores original behavior.""" + enable_notgiven_serialization() + + # Should work with patch enabled + json.dumps({"key": NOT_GIVEN}) + + # Disable the patch + disable_notgiven_serialization() + + # Should fail again + with pytest.raises(TypeError, match="not JSON serializable"): + json.dumps({"key": NOT_GIVEN}) + + +def test_notgiven_instance_check(): + """Test that NotGiven instance checking works correctly.""" + assert isinstance(NOT_GIVEN, NotGiven) + assert not isinstance(None, NotGiven) + assert not isinstance("NOT_GIVEN", NotGiven) + + +def test_notgiven_boolean_behavior(): + """Test that NotGiven behaves correctly in boolean contexts.""" + # NotGiven should evaluate to False + assert not NOT_GIVEN + assert bool(NOT_GIVEN) is False + + +def test_notgiven_repr(): + """Test that NotGiven has proper string representation.""" + assert repr(NOT_GIVEN) == "NOT_GIVEN" From 95ab61d173896388f234418685f23c56c841b639 Mon Sep 17 00:00:00 2001 From: vrushankportkey Date: Tue, 18 Nov 2025 00:30:22 +0530 Subject: [PATCH 2/5] update --- examples/notgiven_serialization_example.py | 174 --------------------- portkey_ai/_vendor/openai/_types.py | 12 -- portkey_ai/utils/json_utils.py | 15 +- tests/test_notgiven_serialization.py | 121 -------------- 4 files changed, 10 insertions(+), 312 deletions(-) delete mode 100644 examples/notgiven_serialization_example.py delete mode 100644 tests/test_notgiven_serialization.py diff --git a/examples/notgiven_serialization_example.py b/examples/notgiven_serialization_example.py deleted file mode 100644 index cd22f29e..00000000 --- a/examples/notgiven_serialization_example.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Example: Using Portkey with openai-agents (NotGiven Serialization) - -This example demonstrates that Portkey now automatically handles NotGiven -serialization out of the box. No manual setup required! - -The fix is transparent - just import and use Portkey normally. -""" - -import portkey_ai -from portkey_ai import AsyncPortkey, enable_notgiven_serialization, disable_notgiven_serialization -import json - - -def example_automatic_serialization(): - """Example showing automatic serialization works out of the box.""" - print("=== Automatic Serialization (No Setup Needed!) ===\n") - - from portkey_ai._vendor.openai._types import NOT_GIVEN - - # Data that contains NotGiven objects - data = { - "model": "gpt-4", - "temperature": 0.7, - "optional_param": NOT_GIVEN, # This works automatically now! - "another_param": None, - } - - # Use standard json.dumps - no special encoder needed! - try: - json_string = json.dumps(data) # Just works! ✨ - print(f"✅ Serialization works automatically: {json_string}\n") - except TypeError as e: - print(f"❌ Serialization failed: {e}\n") - - -def example_basic_usage(): - """Basic example of using PortkeyJSONEncoder for manual serialization.""" - print("=== Custom Encoder (Optional) ===\n") - - from portkey_ai import PortkeyJSONEncoder - from portkey_ai._vendor.openai._types import NOT_GIVEN - - # You can still use the custom encoder if you prefer - data = { - "model": "gpt-4", - "temperature": 0.7, - "optional_param": NOT_GIVEN, - "another_param": None, - } - - # Serialize using PortkeyJSONEncoder - try: - json_string = json.dumps(data, cls=PortkeyJSONEncoder) - print(f"✅ Successfully serialized with encoder: {json_string}\n") - except TypeError as e: - print(f"❌ Serialization failed: {e}\n") - - -def example_global_serialization(): - """Example showing disable/enable functionality.""" - print("=== Manual Enable/Disable Example ===\n") - - from portkey_ai._vendor.openai._types import NOT_GIVEN - - print("Note: Serialization is already enabled automatically!") - print("But you can disable and re-enable if needed:\n") - - # Disable temporarily - print("1. Disabling serialization...") - disable_notgiven_serialization() - - data = {"param": NOT_GIVEN} - - try: - json.dumps(data) - print(" ✅ Still works (unexpected)") - except TypeError: - print(" ✅ Correctly disabled - can't serialize NotGiven") - - # Re-enable - print("\n2. Re-enabling serialization...") - enable_notgiven_serialization() - - try: - json_string = json.dumps(data) - print(f" ✅ Works again: {json_string}\n") - except TypeError as e: - print(f" ❌ Failed: {e}\n") - - -def example_with_portkey_client(): - """Example showing how to use with a Portkey client.""" - print("=== Portkey Client Example ===\n") - - print("Serialization is enabled automatically - no setup needed!") - - # Create Portkey client - serialization works out of the box! - client = AsyncPortkey( - api_key="your-portkey-api-key", # or set PORTKEY_API_KEY env var - virtual_key="your-virtual-key", # optional - ) - - print("✅ AsyncPortkey client created successfully") - print("✅ The client can be serialized by external libraries like openai-agents") - - # Example: Simulate what openai-agents might do - try: - # Try to serialize the client's attributes - client_dict = { - "api_key": client.api_key, - "base_url": str(client.base_url), - "virtual_key": client.virtual_key, - } - serialized = json.dumps(client_dict) - print(f"✅ Client attributes serialized successfully: {serialized}\n") - except TypeError as e: - print(f"❌ Client serialization failed: {e}\n") - - -def example_before_and_after(): - """Demonstrate the problem and solution side by side.""" - print("=== Before and After Comparison ===\n") - - from portkey_ai._vendor.openai._types import NOT_GIVEN - - data = {"param": NOT_GIVEN} - - # BEFORE: This would fail - print("Before enabling global serialization:") - try: - json.dumps(data) - print("✅ Serialization succeeded (unexpected)") - except TypeError as e: - print(f"❌ Serialization failed as expected: {str(e)[:50]}...") - - # AFTER: This works - print("\nAfter enabling global serialization:") - enable_notgiven_serialization() - try: - result = json.dumps(data) - print(f"✅ Serialization succeeded: {result}") - except TypeError as e: - print(f"❌ Serialization failed (unexpected): {e}") - - from portkey_ai import disable_notgiven_serialization - disable_notgiven_serialization() - print() - - -def main(): - """Run all examples.""" - print("\n" + "="*60) - print("Portkey NotGiven Serialization Examples") - print("="*60 + "\n") - - print("✨ Good News: Serialization works automatically!") - print(" No manual setup required - just import and use.\n") - - example_automatic_serialization() - example_with_portkey_client() - example_basic_usage() - example_global_serialization() - example_before_and_after() - - print("="*60) - print("\n✨ All examples completed!") - print("\n📚 Key Takeaway: NotGiven serialization works out of the box!") - print(" For more information, see docs/notgiven-serialization.md") - print("="*60 + "\n") - - -if __name__ == "__main__": - main() diff --git a/portkey_ai/_vendor/openai/_types.py b/portkey_ai/_vendor/openai/_types.py index 28642055..2387d7e0 100644 --- a/portkey_ai/_vendor/openai/_types.py +++ b/portkey_ai/_vendor/openai/_types.py @@ -143,18 +143,6 @@ def __bool__(self) -> Literal[False]: def __repr__(self) -> str: return "NOT_GIVEN" - def __reduce__(self) -> tuple[type[NotGiven], tuple[()]]: - """Support for pickling/serialization.""" - return (self.__class__, ()) - - def __copy__(self) -> NotGiven: - """Return self since NotGiven is a singleton-like sentinel.""" - return self - - def __deepcopy__(self, memo: dict[int, Any]) -> NotGiven: - """Return self since NotGiven is a singleton-like sentinel.""" - return self - not_given = NotGiven() # for backwards compatibility: diff --git a/portkey_ai/utils/json_utils.py b/portkey_ai/utils/json_utils.py index 26821ee1..37720277 100644 --- a/portkey_ai/utils/json_utils.py +++ b/portkey_ai/utils/json_utils.py @@ -1,13 +1,15 @@ import json from portkey_ai._vendor.openai._types import NotGiven +from portkey_ai._vendor.openai._utils import is_given class PortkeyJSONEncoder(json.JSONEncoder): - """Custom JSON encoder that handles Portkey-specific types like NotGiven.""" + """Custom JSON encoder that handles NotGiven types using OpenAI's utilities.""" def default(self, obj): - if isinstance(obj, NotGiven): - # Return None for NotGiven instances during JSON serialization + # Use OpenAI's is_given utility to check for NotGiven/Omit + if not is_given(obj): + # Return None for NotGiven/Omit instances during JSON serialization return None return super().default(obj) @@ -17,14 +19,17 @@ def default(self, obj): def enable_notgiven_serialization(): """ - Enable global JSON serialization support for NotGiven types. + Enable global JSON serialization support for NotGiven/Omit types. + + Uses OpenAI's is_given utility to detect sentinel values. """ global _original_json_encoder if _original_json_encoder is None: _original_json_encoder = json.JSONEncoder.default def patched_default(self, obj): - if isinstance(obj, NotGiven): + # Use OpenAI's utility to check for NotGiven/Omit + if not is_given(obj): return None return _original_json_encoder(self, obj) diff --git a/tests/test_notgiven_serialization.py b/tests/test_notgiven_serialization.py deleted file mode 100644 index 06e1e68e..00000000 --- a/tests/test_notgiven_serialization.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Test NotGiven serialization functionality. - -This test verifies that NotGiven sentinel objects can be properly serialized -to JSON, which is necessary for compatibility with external libraries like -openai-agents that may attempt to serialize client objects for logging/tracing. -""" - -import json -import pytest -from portkey_ai._vendor.openai._types import NOT_GIVEN, NotGiven -from portkey_ai.utils.json_utils import ( - PortkeyJSONEncoder, - enable_notgiven_serialization, - disable_notgiven_serialization, -) - - -def test_notgiven_with_custom_encoder(): - """Test that PortkeyJSONEncoder can serialize NotGiven objects.""" - test_obj = { - "key1": "value1", - "key2": NOT_GIVEN, - "key3": 123, - "nested": { - "key4": NOT_GIVEN, - "key5": "value5" - } - } - - # Should not raise TypeError - result = json.dumps(test_obj, cls=PortkeyJSONEncoder) - parsed = json.loads(result) - - # NOT_GIVEN should be serialized as None - assert parsed["key1"] == "value1" - assert parsed["key2"] is None - assert parsed["key3"] == 123 - assert parsed["nested"]["key4"] is None - assert parsed["nested"]["key5"] == "value5" - - -def test_notgiven_in_list(): - """Test that NotGiven objects in lists are properly serialized.""" - test_list = [1, "test", NOT_GIVEN, {"key": NOT_GIVEN}] - - result = json.dumps(test_list, cls=PortkeyJSONEncoder) - parsed = json.loads(result) - - assert parsed[0] == 1 - assert parsed[1] == "test" - assert parsed[2] is None - assert parsed[3]["key"] is None - - -def test_enable_notgiven_serialization(): - """Test that enable_notgiven_serialization allows standard json.dumps to work.""" - # Note: Serialization is now enabled by default when portkey_ai is imported - # This test verifies it works correctly - - # It should work automatically (already enabled by module import) - result = json.dumps({"key": NOT_GIVEN}) - parsed = json.loads(result) - assert parsed["key"] is None - - # Test with nested structures - complex_obj = { - "a": NOT_GIVEN, - "b": [1, NOT_GIVEN, 3], - "c": {"nested": NOT_GIVEN} - } - result = json.dumps(complex_obj) - parsed = json.loads(result) - assert parsed["a"] is None - assert parsed["b"] == [1, None, 3] - assert parsed["c"]["nested"] is None - - # Test that we can disable and re-enable - disable_notgiven_serialization() - with pytest.raises(TypeError, match="not JSON serializable"): - json.dumps({"key": NOT_GIVEN}) - - # Re-enable - enable_notgiven_serialization() - result = json.dumps({"key": NOT_GIVEN}) - parsed = json.loads(result) - assert parsed["key"] is None - - -def test_disable_notgiven_serialization(): - """Test that disable_notgiven_serialization restores original behavior.""" - enable_notgiven_serialization() - - # Should work with patch enabled - json.dumps({"key": NOT_GIVEN}) - - # Disable the patch - disable_notgiven_serialization() - - # Should fail again - with pytest.raises(TypeError, match="not JSON serializable"): - json.dumps({"key": NOT_GIVEN}) - - -def test_notgiven_instance_check(): - """Test that NotGiven instance checking works correctly.""" - assert isinstance(NOT_GIVEN, NotGiven) - assert not isinstance(None, NotGiven) - assert not isinstance("NOT_GIVEN", NotGiven) - - -def test_notgiven_boolean_behavior(): - """Test that NotGiven behaves correctly in boolean contexts.""" - # NotGiven should evaluate to False - assert not NOT_GIVEN - assert bool(NOT_GIVEN) is False - - -def test_notgiven_repr(): - """Test that NotGiven has proper string representation.""" - assert repr(NOT_GIVEN) == "NOT_GIVEN" From d4c9a28fd0dd234346dde16862f08287f264357d Mon Sep 17 00:00:00 2001 From: vrushankportkey Date: Tue, 18 Nov 2025 01:24:55 +0530 Subject: [PATCH 3/5] Update json_utils.py --- portkey_ai/utils/json_utils.py | 80 ++++++++++++++++------------------ 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/portkey_ai/utils/json_utils.py b/portkey_ai/utils/json_utils.py index 37720277..f99299a4 100644 --- a/portkey_ai/utils/json_utils.py +++ b/portkey_ai/utils/json_utils.py @@ -1,80 +1,74 @@ import json -from portkey_ai._vendor.openai._types import NotGiven from portkey_ai._vendor.openai._utils import is_given +_BASE_JSON_DEFAULT = json.JSONEncoder.default +_patched_notgiven_serialization = False + + class PortkeyJSONEncoder(json.JSONEncoder): - """Custom JSON encoder that handles NotGiven types using OpenAI's utilities.""" + """Custom JSON encoder that handles NotGiven/Omit types using OpenAI's utilities.""" def default(self, obj): - # Use OpenAI's is_given utility to check for NotGiven/Omit + # Use OpenAI's is_given utility to check for NotGiven/Omit sentinels if not is_given(obj): # Return None for NotGiven/Omit instances during JSON serialization return None return super().default(obj) -_original_json_encoder = None - - def enable_notgiven_serialization(): - """ - Enable global JSON serialization support for NotGiven/Omit types. + """Enable global JSON serialization support for NotGiven/Omit types. Uses OpenAI's is_given utility to detect sentinel values. """ - global _original_json_encoder - if _original_json_encoder is None: - _original_json_encoder = json.JSONEncoder.default - - def patched_default(self, obj): - # Use OpenAI's utility to check for NotGiven/Omit - if not is_given(obj): - return None - return _original_json_encoder(self, obj) - - json.JSONEncoder.default = patched_default + global _patched_notgiven_serialization + if _patched_notgiven_serialization: + return + + def patched_default(self, obj): + # Use OpenAI's utility to check for NotGiven/Omit + if not is_given(obj): + return None + return _BASE_JSON_DEFAULT(self, obj) + + json.JSONEncoder.default = patched_default + _patched_notgiven_serialization = True def disable_notgiven_serialization(): - """ - Disable global JSON serialization support for NotGiven types. + """Disable global JSON serialization support for NotGiven/Omit types. This restores the original JSONEncoder behavior. """ - global _original_json_encoder - if _original_json_encoder is not None: - json.JSONEncoder.default = _original_json_encoder - _original_json_encoder = None + global _patched_notgiven_serialization + if not _patched_notgiven_serialization: + return + json.JSONEncoder.default = _BASE_JSON_DEFAULT + _patched_notgiven_serialization = False -def serialize_kwargs(**kwargs): - # Function to check if a value is serializable - def is_serializable(value): - try: - json.dumps(value, cls=PortkeyJSONEncoder) - return True - except (TypeError, ValueError): - return False +def _is_serializable(value) -> bool: + """Return True if value can be serialized with PortkeyJSONEncoder.""" + try: + json.dumps(value, cls=PortkeyJSONEncoder) + return True + except (TypeError, ValueError): + return False + + +def serialize_kwargs(**kwargs): # Filter out non-serializable items - serializable_kwargs = {k: v for k, v in kwargs.items() if is_serializable(v)} + serializable_kwargs = {k: v for k, v in kwargs.items() if _is_serializable(v)} # Convert to string representation return json.dumps(serializable_kwargs, cls=PortkeyJSONEncoder) def serialize_args(*args): - # Function to check if a value is serializable - def is_serializable(value): - try: - json.dumps(value, cls=PortkeyJSONEncoder) - return True - except (TypeError, ValueError): - return False - # Filter out non-serializable items - serializable_args = [arg for arg in args if is_serializable(arg)] + serializable_args = [arg for arg in args if _is_serializable(arg)] # Convert to string representation return json.dumps(serializable_args, cls=PortkeyJSONEncoder) From 95a317f8facea0e1eb4d3d119717952fab169f30 Mon Sep 17 00:00:00 2001 From: vrushankportkey Date: Tue, 18 Nov 2025 01:44:05 +0530 Subject: [PATCH 4/5] update --- portkey_ai/utils/json_utils.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/portkey_ai/utils/json_utils.py b/portkey_ai/utils/json_utils.py index f99299a4..6d54fe3f 100644 --- a/portkey_ai/utils/json_utils.py +++ b/portkey_ai/utils/json_utils.py @@ -7,27 +7,28 @@ class PortkeyJSONEncoder(json.JSONEncoder): - """Custom JSON encoder that handles NotGiven/Omit types using OpenAI's utilities.""" - - def default(self, obj): - # Use OpenAI's is_given utility to check for NotGiven/Omit sentinels + """JSON encoder that treats OpenAI/Portkey "not provided" markers as null.""" + + def default(self, obj): # type: ignore[override] + # If this is one of OpenAI's internal "not provided" / omit markers, + # encode it as None (null in JSON) instead of raising TypeError. if not is_given(obj): - # Return None for NotGiven/Omit instances during JSON serialization return None return super().default(obj) -def enable_notgiven_serialization(): - """Enable global JSON serialization support for NotGiven/Omit types. - - Uses OpenAI's is_given utility to detect sentinel values. +def enable_notgiven_serialization() -> None: + """Enable global JSON support for OpenAI/Portkey "not provided" markers. + + After this is called (done automatically in portkey_ai.__init__), any + json.dumps(...) that encounters these markers will encode them as null + instead of raising a TypeError. """ global _patched_notgiven_serialization if _patched_notgiven_serialization: return - def patched_default(self, obj): - # Use OpenAI's utility to check for NotGiven/Omit + def patched_default(self, obj): # type: ignore[override] if not is_given(obj): return None return _BASE_JSON_DEFAULT(self, obj) @@ -36,10 +37,10 @@ def patched_default(self, obj): _patched_notgiven_serialization = True -def disable_notgiven_serialization(): - """Disable global JSON serialization support for NotGiven/Omit types. - - This restores the original JSONEncoder behavior. +def disable_notgiven_serialization() -> None: + """Disable global JSON support for OpenAI/Portkey "not provided" markers. + + Restores the original JSONEncoder.default implementation. """ global _patched_notgiven_serialization if not _patched_notgiven_serialization: From a6b67c5c22e89b4606fee43e855a8e8750e53ff7 Mon Sep 17 00:00:00 2001 From: vrushankportkey Date: Tue, 18 Nov 2025 02:48:41 +0530 Subject: [PATCH 5/5] update --- portkey_ai/__init__.py | 3 +-- portkey_ai/utils/json_utils.py | 33 +++++++++++---------------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/portkey_ai/__init__.py b/portkey_ai/__init__.py index 3a257a83..52fd9344 100644 --- a/portkey_ai/__init__.py +++ b/portkey_ai/__init__.py @@ -169,8 +169,7 @@ enable_notgiven_serialization, disable_notgiven_serialization, ) - -# Automatically enable NotGiven serialization. Users can call disable_notgiven_serialization() if needed. + enable_notgiven_serialization() api_key = os.environ.get(PORTKEY_API_KEY_ENV) diff --git a/portkey_ai/utils/json_utils.py b/portkey_ai/utils/json_utils.py index 6d54fe3f..a302dcbc 100644 --- a/portkey_ai/utils/json_utils.py +++ b/portkey_ai/utils/json_utils.py @@ -18,12 +18,7 @@ def default(self, obj): # type: ignore[override] def enable_notgiven_serialization() -> None: - """Enable global JSON support for OpenAI/Portkey "not provided" markers. - - After this is called (done automatically in portkey_ai.__init__), any - json.dumps(...) that encounters these markers will encode them as null - instead of raising a TypeError. - """ + """Globally encode NotGiven / Omit markers as null in json.dumps.""" global _patched_notgiven_serialization if _patched_notgiven_serialization: return @@ -38,10 +33,7 @@ def patched_default(self, obj): # type: ignore[override] def disable_notgiven_serialization() -> None: - """Disable global JSON support for OpenAI/Portkey "not provided" markers. - - Restores the original JSONEncoder.default implementation. - """ + """Restore the original json.JSONEncoder.default implementation.""" global _patched_notgiven_serialization if not _patched_notgiven_serialization: return @@ -51,25 +43,22 @@ def disable_notgiven_serialization() -> None: def _is_serializable(value) -> bool: - """Return True if value can be serialized with PortkeyJSONEncoder.""" try: json.dumps(value, cls=PortkeyJSONEncoder) - return True except (TypeError, ValueError): return False + return True def serialize_kwargs(**kwargs): - # Filter out non-serializable items - serializable_kwargs = {k: v for k, v in kwargs.items() if _is_serializable(v)} - - # Convert to string representation - return json.dumps(serializable_kwargs, cls=PortkeyJSONEncoder) + return json.dumps( + {k: v for k, v in kwargs.items() if _is_serializable(v)}, + cls=PortkeyJSONEncoder, + ) def serialize_args(*args): - # Filter out non-serializable items - serializable_args = [arg for arg in args if _is_serializable(arg)] - - # Convert to string representation - return json.dumps(serializable_args, cls=PortkeyJSONEncoder) + return json.dumps( + [arg for arg in args if _is_serializable(arg)], + cls=PortkeyJSONEncoder, + )