Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -727,11 +727,42 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An
# response format
if response_format := options.get("response_format"):
if isinstance(response_format, dict):
run_options["response_format"] = response_format
run_options["response_format"] = self._normalize_response_format_dict(
cast("dict[str, Any]", response_format)
)
else:
run_options["response_format"] = type_to_response_format_param(response_format)
Comment thread
exp-ouroborous marked this conversation as resolved.
return run_options

@staticmethod
def _normalize_response_format_dict(response_format: dict[str, Any]) -> dict[str, Any]:
"""Wrap raw JSON schemas (e.g. ``{"type": "object", ...}``) in the json_schema envelope.

Mirrors the Responses client's ``_convert_response_format`` handling of raw
schemas so both clients accept the same inputs; dicts already using a valid
Chat Completions response_format type pass through unchanged.
"""
format_type = response_format.get("type")
if format_type in {"json_schema", "json_object", "text"}:
return response_format
# Detect raw JSON Schema by primitive type or known schema keywords.
json_schema_keywords = {"properties", "anyOf", "oneOf", "allOf", "$ref", "$defs"}
json_schema_primitive_types = {"object", "array", "string", "number", "integer", "boolean", "null"}
if format_type in json_schema_primitive_types or (
format_type is None and any(k in response_format for k in json_schema_keywords)
):
schema = dict(response_format)
if schema.get("type") == "object" and "additionalProperties" not in schema:
schema["additionalProperties"] = False
# Pop title from schema since OpenAI strict mode rejects unknown keys;
# use it as the schema name in the envelope instead.
name = str(schema.pop("title", None) or "response")
return {
"type": "json_schema",
"json_schema": {"name": name, "schema": schema, "strict": True},
}
return response_format

def _parse_response_from_openai(self, response: ChatCompletion, options: Mapping[str, Any]) -> ChatResponse:
"""Parse a response from OpenAI into a ChatResponse."""
response_metadata = self._get_metadata_from_chat_response(response)
Expand Down
21 changes: 21 additions & 0 deletions python/packages/openai/tests/openai/test_openai_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6310,6 +6310,27 @@ async def get_api_key() -> str:
True,
id="response_format_runtime_json_schema",
),
param(
"response_format",
{
"title": "WeatherDigest",
"type": "object",
"properties": {
"location": {"type": "string"},
"conditions": {"type": "string"},
"temperature_c": {"type": "number"},
"advisory": {"type": "string"},
},
"required": [
"location",
"conditions",
"temperature_c",
"advisory",
],
},
True,
id="response_format_raw_json_schema",
),
],
)
async def test_integration_options(
Expand Down
Comment thread
eavanvalkenburg marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,59 @@ def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str])
assert prepared_options["response_format"] == custom_format


def test_response_format_raw_schema_dict_is_wrapped(openai_unit_test_env: dict[str, str]) -> None:
"""A raw JSON-Schema dict is wrapped in the json_schema envelope (parity with the Responses client)."""
client = OpenAIChatCompletionClient()

messages = [Message(role="user", contents=["test"])]
raw_schema = {
"type": "object",
"properties": {"word": {"type": "string"}},
"required": ["word"],
}

prepared_options = client._prepare_options(messages, {"response_format": raw_schema})

assert prepared_options["response_format"] == {
"type": "json_schema",
"json_schema": {
"name": "response",
"schema": {
"type": "object",
"properties": {"word": {"type": "string"}},
"required": ["word"],
"additionalProperties": False,
},
"strict": True,
},
}


def test_response_format_raw_schema_title_becomes_name(openai_unit_test_env: dict[str, str]) -> None:
"""A raw schema's title is popped into the envelope name (strict mode rejects unknown keys)."""
client = OpenAIChatCompletionClient()

messages = [Message(role="user", contents=["test"])]
raw_schema = {"type": "object", "title": "Word", "properties": {"word": {"type": "string"}}}

prepared_options = client._prepare_options(messages, {"response_format": raw_schema})

wrapped = prepared_options["response_format"]
assert wrapped["json_schema"]["name"] == "Word"
assert "title" not in wrapped["json_schema"]["schema"]


def test_response_format_json_object_dict_passthrough(openai_unit_test_env: dict[str, str]) -> None:
"""Valid non-json_schema response_format types still pass through unchanged."""
client = OpenAIChatCompletionClient()

messages = [Message(role="user", contents=["test"])]

prepared_options = client._prepare_options(messages, {"response_format": {"type": "json_object"}})

assert prepared_options["response_format"] == {"type": "json_object"}


def test_parse_response_with_dict_response_format(openai_unit_test_env: dict[str, str]) -> None:
"""Chat completions should parse dict response_format values into response.value."""
client = OpenAIChatCompletionClient()
Expand Down Expand Up @@ -1786,6 +1839,27 @@ class OutputStruct(BaseModel):
True,
id="response_format_runtime_json_schema",
),
param(
"response_format",
{
"title": "WeatherDigest",
"type": "object",
"properties": {
"location": {"type": "string"},
"conditions": {"type": "string"},
"temperature_c": {"type": "number"},
"advisory": {"type": "string"},
},
"required": [
"location",
"conditions",
"temperature_c",
"advisory",
],
},
True,
id="response_format_raw_json_schema",
),
],
)
async def test_integration_options(
Expand Down
Loading