From 2351bd59c81610736f63cf331f13c9bef4c30c57 Mon Sep 17 00:00:00 2001 From: Binit Mohanty Date: Sun, 19 Jul 2026 10:24:13 -0500 Subject: [PATCH 1/2] Python: Fix OpenAIChatCompletionClient passing raw JSON-Schema dict response_format through unwrapped Raw schema dicts (e.g. {"type": "object", ...}) were forwarded to the Chat Completions API verbatim, which OpenAI rejects with a 400. The Responses client already auto-wraps the same input. Mirror its raw-schema detection (primitive types / schema keywords), wrap into the {"type": "json_schema", "json_schema": {...}} envelope with additionalProperties: false injection and title -> name promotion, and leave already-valid response_format dicts untouched. Fixes #7197 (cherry picked from commit dce5c3b06328fbde45eb2a9a25638af5b1ec85e3) --- .../_chat_completion_client.py | 31 ++++++++++- .../test_openai_chat_completion_client.py | 53 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_completion_client.py b/python/packages/openai/agent_framework_openai/_chat_completion_client.py index 57f74bfdc71..d546f2fbf12 100644 --- a/python/packages/openai/agent_framework_openai/_chat_completion_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_completion_client.py @@ -694,11 +694,40 @@ 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(response_format) else: run_options["response_format"] = type_to_response_format_param(response_format) 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) diff --git a/python/packages/openai/tests/openai/test_openai_chat_completion_client.py b/python/packages/openai/tests/openai/test_openai_chat_completion_client.py index 81d9963688b..00961ad25ff 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_completion_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_completion_client.py @@ -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() From 47e3a08c15c11bc701377c4ad252afbdc011c97d Mon Sep 17 00:00:00 2001 From: Binit Mohanty Date: Mon, 20 Jul 2026 06:12:36 -0500 Subject: [PATCH 2/2] Python: Add live integration coverage for raw JSON-Schema response_format dicts Adds a response_format_raw_json_schema param to test_integration_options in both the Chat Completions and Responses client test suites, proving the same bare schema dict (title set, additionalProperties omitted) round-trips through both live APIs and yields parsed structured output. Co-Authored-By: Claude Fable 5 --- .../tests/openai/test_openai_chat_client.py | 21 +++++++++++++++++++ .../test_openai_chat_completion_client.py | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 8cc7fb737ab..18fb7e5e741 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -5567,6 +5567,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( diff --git a/python/packages/openai/tests/openai/test_openai_chat_completion_client.py b/python/packages/openai/tests/openai/test_openai_chat_completion_client.py index 00961ad25ff..c3dc14ea384 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_completion_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_completion_client.py @@ -1839,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(