Is there a way to enable tool calling and structured output in the same agent?

I am having trouble using these two functionalities together (in the same agent). The goal is to allow the agent access to some tools, and then have the final response follow a defined Structured Output (OpenAI). When I use the agent with only one of the features, it works as intended, but when combining them, the response format defined makes it impossible for the LLM to match the required inputs for the tools.

Example of successful usage only using one component at a time:

llm_with_tools = self.llm.bind_tools(
                tools=tools,
                parallel_tool_calls=self.parallel_tool_calls,
            )
## OR

llm_with_struct_output = self.llm.with_structured_output(
                schema=InternalResult
            )

Example of simultaneous usage (both functionalities in the same agent):

unified_model = self.llm.with_structured_output(
                InternalResult,
                method="json_schema",
                include_raw=True,
                strict=True,
                tools=tools
            )
## OR

llm_with_tools = self.llm.bind_tools(
                tools=tools,
                parallel_tool_calls=self.parallel_tool_calls,
            ).with_structured_output(schema=InternalResult)

These last examples always produce the following error, due to a mismatch between the tool’s required input and LLM output:

Error: Error code: 400 - {‘error’: {‘message’: \“Invalid schema for function ‘get_company_news’: In context=(), ‘additionalProperties’ is required to be supplied and to be false.\”, ‘type’: ‘invalid_request_error’}}

Currently, the workaround I am using is doing 2 different LLM calls (one for tool calling, another for formatting) but this adds a lot of time and cost to the process. Another possisbility is to have the Structured Output schema as a tool and guide the agent to always call it, but this approach is not as robust as I wished.

Is there any other way to create an agent with these two functionalities?

Thank you!

hi @VBargas

what’s the definition of get_company_news tool? Could you share the code?

Short answer for now: you can have tools and structured output in the same “agent”, but you can’t mix them the way you’re doing with method="json_schema", strict=True unless all of your tools use a “strict” JSON Schema. Right now the OpenAI JSON‑schema path is rejecting your tool definitions, so the whole call fails before the LLM ever gets to reason about tools vs. final schema.

Hi @pawel-twardziak!

the ‘get_company_news’ tool is coming from an MCP. Here is the JSON definition:

“{"type": "function", "function": {"name": "get_company_news", "description": "Financial News API (spec-aligned).\n\nArgs:\n ticker (str, optional): SYMBOL.EXCHANGE_ID (e.g., ‘``AAPL.US``’). Mapped to ‘s’.\n tag (str, optional): Topic tag (e.g., ‘technology’). Mapped to ‘t’.\n start_date (str, optional): YYYY-MM-DD. Mapped to ‘from’.\n end_date (str, optional): YYYY-MM-DD. Mapped to ‘to’.\n limit (int): 1..1000 (default 50).\n offset (int): >= 0 (default 0).\n fmt (str): ‘json’ or ‘xml’ (default ‘json’).\n api_token (str, optional): Per-call token override; env token used if omitted.\n\nReturns:\n str: JSON string of articles (or {\"xml\": \"…\"} if fmt=‘xml’ and your client returns text).", "parameters": {"properties": {"ticker": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Parameter ticker"}, "tag": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Parameter tag"}, "start_date": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Parameter start_date"}, "end_date": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Parameter end_date"}, "limit": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Parameter limit"}, "offset": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Parameter offset"}, "fmt": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Parameter fmt"}, "api_token": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Parameter api_token"}}, "type": "object"}}}”

Regarding the method I am currently using (with_structured_output() from class ChatOpenAI), I had it set with these attributes because in the documentation I found this about the tools attribute:

tools:
 
  A list of tool-like objects to bind to the chat model. Requires that:

    - `method` is `'json_schema'` (default).
    - `strict=True`
    - `include_raw=True`

  If a model elects to call a tool, the resulting `AIMessage` in `'raw'` will                         include tool calls.

With the tools I am using all coming from an MCP, is there a way to deal with this ‘strict’ issue (assuming this is the problem)?

Thanks in advance

Can you manipulate the JSON schema for the MCP tool?

You could add "additionalProperties": false at the same level as "properties" and "type" within the parameters object like this:

{
  type: "function",
  function: {
    name: "get_company_news",
    description: "Financial News API (spec-aligned)...",
    parameters: {
      type: "object",
      properties: {
        ticker: {
          anyOf: [
            {
              type: "string"
            },
            {
              type: "null"
            }
          ],
          default: null,
          description: "Parameter ticker"
        },
        tag: {
          anyOf: [
            {
              type: "string"
            },
            {
              type: "null"
            }
          ],
          default: null,
          description: "Parameter tag"
        },
        // ... other properties ...
      },
      additionalProperties: false
    }
  }
}

Or can you just turn off the strict attribute?

`strict=False`