Skip to content

Python: [Bug]: OpenAIChatCompletionClient passes raw JSON-Schema dict response_format through unwrapped (OpenAIChatClient/Responses auto-wraps the same input) #7197

Description

@exp-ouroborous

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).

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Fields

No fields configured for Bug.

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions