Description
When response_format is a raw JSON-Schema dict (e.g. {"type": "object", "properties": {...}}), OpenAIChatCompletionClient forwards it to the Chat Completions API unchanged, and OpenAI rejects it with a 400 (response_format.type must be json_schema / json_object / text). The Responses-based OpenAIChatClient handles the identical input correctly by auto-wrapping it into a json_schema format block — so the two clients disagree on the same input.
Verified on agent-framework-openai 1.10.1 by capturing the outgoing request (mocked transport, no network) — see Code Sample. The passthrough is here on current main (_chat_completion_client.py#L695-L699 @ 7c6b1e975f75):
if response_format := options.get("response_format"):
if isinstance(response_format, dict):
run_options["response_format"] = response_format # raw dict, no wrapping
else:
run_options["response_format"] = type_to_response_format_param(response_format)
Only Pydantic models (the non-dict branch) get converted. This bites the declarative path in particular: the loader sets chat_options["response_format"] = outputSchema.to_json_schema(), which produces exactly this bare-dict shape — so a declarative agent with an outputSchema fails on the Chat Completions client but works on the Responses client.
Suggested fix: mirror the Responses client's wrapping — if the dict is not already a valid response_format param (top-level "type" not one of json_schema / json_object / text), wrap it as {"type": "json_schema", "json_schema": {"name": ..., "schema": <dict>, "strict": ...}}.
Code Sample
# Deterministic no-network repro on agent-framework-openai==1.10.1:
# capture what each client would send to OpenAI for the SAME schema dict.
import asyncio, json
from agent_framework import Message
from agent_framework_openai import OpenAIChatClient, OpenAIChatCompletionClient
schema = {"type": "object", "properties": {"word": {"type": "string"}}, "required": ["word"]}
captured = {}
async def fake_create(**kwargs):
captured.update(kwargs)
raise RuntimeError("captured")
async def main():
cc = OpenAIChatCompletionClient(model="gpt-4o-mini", api_key="sk-fake")
cc.client.chat.completions.create = fake_create
try:
await cc.get_response([Message(role="user", contents=["Give me a word."])],
options={"response_format": schema})
except Exception:
pass
print("ChatCompletions sends:", json.dumps(captured.get("response_format")))
captured.clear()
rc = OpenAIChatClient(model="gpt-4o-mini", api_key="sk-fake")
rc.client.responses.create = fake_create
try:
await rc.get_response([Message(role="user", contents=["Give me a word."])],
options={"response_format": schema})
except Exception:
pass
print("Responses sends: ", json.dumps(captured.get("text")))
asyncio.run(main())
Output (1.10.1):
ChatCompletions sends: {"type": "object", "properties": {"word": {"type": "string"}}, "required": ["word"]}
Responses sends: {"format": {"type": "json_schema", "name": "response", "schema": {"type": "object", "properties": {"word": {"type": "string"}}, "required": ["word"], "additionalProperties": false}, "strict": true}}
The Chat Completions payload is rejected by OpenAI; the Responses payload is accepted.
Error Messages / Stack Traces
openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid value: 'object'. Supported values are: 'json_schema', 'json_object', and 'text'.", 'type': 'invalid_request_error', 'param': 'response_format.type', 'code': 'invalid_value'}}
Package Versions
agent-framework-core==1.11.0, agent-framework-openai==1.10.1 (also reproduced on 1.8.1; passthrough present on main @ 7c6b1e9)
Python Version
3.13
Additional Context
Workaround we use in production: declare all affected declarative agents with apiType: Responses. Related but distinct from #1842 (that one is the Pydantic-model branch missing json_schema.name; this one is the dict branch never being wrapped at all).
Description
When
response_formatis a raw JSON-Schema dict (e.g.{"type": "object", "properties": {...}}),OpenAIChatCompletionClientforwards it to the Chat Completions API unchanged, and OpenAI rejects it with a 400 (response_format.typemust bejson_schema/json_object/text). The Responses-basedOpenAIChatClienthandles the identical input correctly by auto-wrapping it into ajson_schemaformat block — so the two clients disagree on the same input.Verified on agent-framework-openai 1.10.1 by capturing the outgoing request (mocked transport, no network) — see Code Sample. The passthrough is here on current main (
_chat_completion_client.py#L695-L699@7c6b1e975f75):Only Pydantic models (the non-dict branch) get converted. This bites the declarative path in particular: the loader sets
chat_options["response_format"] = outputSchema.to_json_schema(), which produces exactly this bare-dict shape — so a declarative agent with anoutputSchemafails on the Chat Completions client but works on the Responses client.Suggested fix: mirror the Responses client's wrapping — if the dict is not already a valid
response_formatparam (top-level"type"not one ofjson_schema/json_object/text), wrap it as{"type": "json_schema", "json_schema": {"name": ..., "schema": <dict>, "strict": ...}}.Code Sample
Output (1.10.1):
The Chat Completions payload is rejected by OpenAI; the Responses payload is accepted.
Error Messages / Stack Traces
Package Versions
agent-framework-core==1.11.0, agent-framework-openai==1.10.1 (also reproduced on 1.8.1; passthrough present on main @ 7c6b1e9)
Python Version
3.13
Additional Context
Workaround we use in production: declare all affected declarative agents with
apiType: Responses. Related but distinct from #1842 (that one is the Pydantic-model branch missingjson_schema.name; this one is the dict branch never being wrapped at all).