Skip to content

OpenAI

Available · Chat Completions and Responses API · no SDK needed

The renderers return plain dicts. Import the functions by name (from agent_adapter.integrations.openai import ...), so the openai SDK name stays free.

Functions

Function (agent_adapter.integrations.openai) Returns
chat_tools(registry, *, strict=True) every tool in Chat Completions format
chat_tool(spec, *, strict=True) one tool: {"type": "function", "function": {...}}
responses_tools(registry, *, strict=True) every tool in Responses API format
responses_tool(spec, *, strict=True) one tool, flat: {"type", "name", "description", "parameters", "strict"}
INTEGRATION the integration object (Chat format); OpenAIIntegration(api="responses", strict=False) for others. bind_context(call) reads call_id, else id

Round trip

Pass the rendered list as tools=. Run each tool call the model sends through one Executor, never with spec.invoke: invoke skips approval, budgets, run guards, the cache and the ledger, so a confirm=True tool would run unapproved.

from agent_adapter import Executor, ToolCall
from agent_adapter.integrations.openai import chat_tools

executor = Executor(registry)  # once per process; no approval port, so confirm tools are denied
tools = chat_tools(registry)  # pass as tools= to chat.completions.create
replies = []
for call in response.choices[0].message.tool_calls:
    # arguments is a JSON string; strict mode sends null for "use the default".
    args = json.loads(call.function.arguments)
    result = await executor.run_call(
        ToolCall(name=call.function.name, arguments=args, id=call.id)
    )
    data = result.to_dict()  # a denial, bad arguments or an unknown tool land in "error"
    payload = data["value"] if result.ok else {"error": data["error"]}
    replies.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(payload)})
sequenceDiagram
  participant App
  participant OA as OpenAI API
  participant Ex as Executor
  App->>OA: chat.completions.create(messages, tools=chat_tools(registry))
  OA-->>App: tool_calls[i]: id, function.name, function.arguments (JSON string)
  App->>Ex: run_call(ToolCall(name, json.loads(arguments), id))
  Ex-->>App: ToolResult (approval, budgets and guards applied)
  App->>OA: {"role": "tool", "tool_call_id": id, "content": value or error}
Step Notes
Executor build it once, with approval= if any tool has confirm=True; with no port such a call is denied (approval)
Several calls the loop runs them in order; executor.run_many(calls) runs them at once and keeps their order
Whole loop run_tool_loop with OpenAIModel sends the calls and answers them for you
Responses API the call is a function_call item: use its name, arguments and call_id, and answer with {"type": "function_call_output", "call_id", "output"}

Strict mode

strict=True (the default) makes every field required and optional ones nullable, and sets additionalProperties: false everywhere. Each argument's default is written into its description (Default: "c".). When the model sends null, validation applies your default. See Strict mode and nulls.

strict=False gives the plain schema: optional fields stay optional and keep default.

Errors to expect

run_call returns every failure as result.error; it never raises for them.

result.error.kind When
tool-not-found the model names a tool you did not register
tool-args-invalid the arguments fail validation; the error tells the model what to fix
approval-denied a confirm tool was denied, or no approval port is set
approval-pending a DeferredApproval port parked the call (human in the loop)

json.loads raises json.JSONDecodeError when the model sends malformed JSON. That comes from the standard library, not the adapter.