From 0308353329606904a905f1e5d39f6e32232c7597 Mon Sep 17 00:00:00 2001 From: piyushrajyadav Date: Fri, 21 Aug 2026 18:25:26 +0530 Subject: [PATCH] fix(realtime): preserve Unicode in session payload serialization (#2428) --- src/openai/lib/_realtime.py | 4 +- tests/test_realtime_calls_unicode.py | 66 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tests/test_realtime_calls_unicode.py diff --git a/src/openai/lib/_realtime.py b/src/openai/lib/_realtime.py index e894d8e451..bee7dc4a8c 100644 --- a/src/openai/lib/_realtime.py +++ b/src/openai/lib/_realtime.py @@ -43,7 +43,7 @@ def create( session_payload = maybe_transform(session, RealtimeSessionCreateRequestParam) files = [ ("sdp", (None, sdp.encode("utf-8"), "application/sdp")), - ("session", (None, json.dumps(session_payload).encode("utf-8"), "application/json")), + ("session", (None, json.dumps(session_payload, ensure_ascii=False).encode("utf-8"), "application/json")), ] return self._post( "/realtime/calls", @@ -80,7 +80,7 @@ async def create( session_payload = await async_maybe_transform(session, RealtimeSessionCreateRequestParam) files = [ ("sdp", (None, sdp.encode("utf-8"), "application/sdp")), - ("session", (None, json.dumps(session_payload).encode("utf-8"), "application/json")), + ("session", (None, json.dumps(session_payload, ensure_ascii=False).encode("utf-8"), "application/json")), ] return await self._post( "/realtime/calls", diff --git a/tests/test_realtime_calls_unicode.py b/tests/test_realtime_calls_unicode.py new file mode 100644 index 0000000000..84109aca74 --- /dev/null +++ b/tests/test_realtime_calls_unicode.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import httpx2 +import pytest + +import openai._legacy_response as _legacy_response +from openai import OpenAI, AsyncOpenAI +from tests.respx2 import MockRouter + + +class TestRealtimeCallsUnicode: + @pytest.mark.respx2(base_url="http://127.0.0.1:4010") + def test_realtime_create_unicode_session_not_escaped(self, client: OpenAI, respx2_mock: MockRouter) -> None: + captured_requests: list[httpx2.Request] = [] + + def side_effect(request: httpx2.Request) -> httpx2.Response: + captured_requests.append(request) + return httpx2.Response(200, json={"id": "call_123"}) + + respx2_mock.post("/realtime/calls").mock(side_effect=side_effect) + + unicode_instructions = "Привет мир! 你好世界 こんにちは" + call = client.realtime.calls.create( + sdp="v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n", + session={ + "type": "realtime", + "instructions": unicode_instructions, + }, + ) + assert isinstance(call, _legacy_response.HttpxBinaryResponseContent) + assert len(captured_requests) == 1 + + body_bytes = captured_requests[0].content + # Ensure the Unicode string is present directly as UTF-8 bytes, not \uXXXX escaped + assert unicode_instructions.encode("utf-8") in body_bytes + # Ensure it's not escaped + assert b"\\u041f\\u0440\\u0438" not in body_bytes + + @pytest.mark.asyncio + @pytest.mark.respx2(base_url="http://127.0.0.1:4010") + async def test_async_realtime_create_unicode_session_not_escaped( + self, async_client: AsyncOpenAI, respx2_mock: MockRouter + ) -> None: + captured_requests: list[httpx2.Request] = [] + + def side_effect(request: httpx2.Request) -> httpx2.Response: + captured_requests.append(request) + return httpx2.Response(200, json={"id": "call_123"}) + + respx2_mock.post("/realtime/calls").mock(side_effect=side_effect) + + unicode_instructions = "Привет мир! 你好世界 こんにちは" + call = await async_client.realtime.calls.create( + sdp="v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n", + session={ + "type": "realtime", + "instructions": unicode_instructions, + }, + ) + assert isinstance(call, _legacy_response.HttpxBinaryResponseContent) + assert len(captured_requests) == 1 + + body_bytes = captured_requests[0].content + # Ensure the Unicode string is present directly as UTF-8 bytes, not \uXXXX escaped + assert unicode_instructions.encode("utf-8") in body_bytes + assert b"\\u041f\\u0440\\u0438" not in body_bytes