diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index 58490e4430..a9a123aec5 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -536,8 +536,14 @@ def _build_request( if is_body_allowed: if isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif kwargs.get("data") is None and not files: + payload = json_data if is_given(json_data) else None + kwargs["content"] = json.dumps( + payload, + ensure_ascii=False, + separators=(",", ":"), + ).encode("utf-8") + headers.setdefault("Content-Type", "application/json") kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/tests/lib/chat/test_completions.py b/tests/lib/chat/test_completions.py index afad5a1391..3a18e8ef8a 100644 --- a/tests/lib/chat/test_completions.py +++ b/tests/lib/chat/test_completions.py @@ -4,6 +4,7 @@ from typing import List, Optional from typing_extensions import Literal, TypeVar +import httpx import pytest from respx import MockRouter from pydantic import Field, BaseModel @@ -161,6 +162,65 @@ class Location(BaseModel): ) +@pytest.mark.respx(base_url=base_url) +def test_parse_pydantic_schema_preserves_unicode(client: OpenAI, respx_mock: MockRouter) -> None: + class EmojiModel(BaseModel): + """Emoji docstring 😍""" + + message: str = Field(description="Say hi 😍") + + body_capture: dict[str, str] = {} + + def handler(request: httpx.Request) -> httpx.Response: + body_capture["content"] = request.content.decode() + return httpx.Response( + 200, + json={ + "id": "chatcmpl-emoji", + "object": "chat.completion", + "created": 0, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": '{"message":"hi 😍"}', + "refusal": None, + }, + "logprobs": None, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + "completion_tokens_details": {"reasoning_tokens": 0}, + }, + "system_fingerprint": "fp_test", + }, + ) + + respx_mock.post("/chat/completions").mock(side_effect=handler) + + client.chat.completions.parse( + model="gpt-4o-mini", + messages=[ + { + "role": "user", + "content": "Say hi", + } + ], + response_format=EmojiModel, + ) + + body = body_capture.get("content", "") + assert body, "expected request body to be captured" + assert "Say hi 😍" in body + assert "\\ud83d" not in body + + @pytest.mark.respx(base_url=base_url) def test_parse_pydantic_model_optional_default( client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch