Connect an OpenAI Responses-Compatible Endpoint
Set up streaming and non-streaming AI Connections against OpenAI or any endpoint that speaks the Responses API schema, and parse the output and tool calls out of both.
Overview
This guide points an AI Connection at the OpenAI Responses API (POST https://api.openai.com/v1/responses), or at any endpoint of your own built to the same schema, and parses both the actual output and the tool calls out of what comes back. No wrapper service, no code.
You'll build two connections against the same endpoint:
- Non-streaming. Response mode
HTTP Response. The whole response object arrives in one body. - Streaming. Response mode
SSE Streaming, with"stream": truein the body. The response arrives as Server-Sent Events, and the tool calls land in the finalresponse.completedframe.
Both extract the same two values: the actual output and the tools called.
Why Tool Calls Need a Transformer
The Responses API doesn't return tool calls in the shape a ToolCall expects, so a key path can't reach them. Three things get in the way:
- Tool calls sit inside
outputalongside everything else.outputis a mixed array ofreasoning,message, andfunction_callitems, ordered however the model produced them. There's no fixed index to point a key path at. - Arguments arrive as a JSON string,
"{\"city\":\"Hong Kong\"}", not an object.inputParametershas to be a real object. - The field names differ. The API calls it
arguments. AToolCallcalls itinputParameters.
A transformer closes all three gaps. You write two of them once, under Project Settings → Transformers, and both connections share them.
Build It
Write the tools called transformer
Go to Project Settings → Transformers → New Transformer, name it
openai_responses_tools, and paste:from typing import Any import json def transformer(data: Any): # Both connections share this transformer, so normalize whatever arrives # into the object that holds the `output` array: # non-streaming: the full response object # streaming: a `response.completed` frame, under "response" # ping preview: a list of every frame received frames = data if isinstance(data, list) else [data] output = [] for frame in frames: if not isinstance(frame, dict): continue response = frame.get("response") if isinstance(frame.get("response"), dict) else frame if isinstance(response.get("output"), list): output = response["output"] tools_called = [] for item in output: if not isinstance(item, dict) or item.get("type") != "function_call": continue arguments = item.get("arguments") if isinstance(arguments, str): try: arguments = json.loads(arguments) except json.JSONDecodeError: arguments = {"raw_arguments": arguments} tools_called.append( { "name": item.get("name"), "inputParameters": arguments if isinstance(arguments, dict) else {}, } ) return tools_calledCheck it with the built-in debugger before saving. Paste a real Responses payload into the Input panel and click Test. You should get back a list of
{"name": ..., "inputParameters": {...}}objects.Write the actual output transformer
Actual output has to be a string, and a Responses call that decides to use a tool usually returns no assistant text at all. Left alone that's an empty actual output, which fails the ping on a streaming connection with "Empty streaming response" and leaves your metrics nothing to score.
Create a second transformer,
openai_responses_output, that falls back to the tool calls when there's no text:from typing import Any def transformer(data: Any): frames = data if isinstance(data, list) else [data] output = [] for frame in frames: if not isinstance(frame, dict): continue response = frame.get("response") if isinstance(frame.get("response"), dict) else frame if isinstance(response.get("output"), list): output = response["output"] texts = [] tool_calls = [] for item in output: if not isinstance(item, dict): continue if item.get("type") == "message": for part in item.get("content") or []: if isinstance(part, dict) and isinstance(part.get("text"), str): texts.append(part["text"]) elif item.get("type") == "function_call": tool_calls.append(f"{item.get('name')}({item.get('arguments')})") if texts: return "".join(texts) # No assistant message, so surface the calls instead of returning "" return "\n".join(tool_calls)Create the non-streaming connection
Go to Project Settings → AI Connections → New AI Connection, name it
OpenAI Responses (non-streaming), and fill in:General → AI App Endpoint
Field Value Endpoint https://api.openai.com/v1/responsesResponse Mode HTTP ResponseRunning your own Responses-compatible endpoint? Put its URL here instead. Every other step in this guide stays the same.
Headers
Key Value AuthorizationBearer sk-...Content-Typeapplication/jsonBody, in JSON payload mode. Type
golden.inputunquoted; the editor encodes it on save, and each golden's input is substituted at request time.{ "model": "gpt-5.4-mini", "input": golden.input, "tools": [ { "type": "function", "name": "get_weather", "description": "Look up the current weather for a city", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "City name, for example San Francisco" } }, "required": ["city"], "additionalProperties": false }, "strict": true } ] }Responses API tools are flat.
type,name,description, andparameterssit at the top level, not nested under afunctionkey the way Chat Completions does it. With"strict": true, every key inpropertiesalso has to appear inrequired, andadditionalPropertieshas to befalse.Output parsing
Parser Setting Actual output Transformer → openai_responses_outputTools called Transformer → openai_responses_toolsLeave retrieval context and state empty. Switch each parser from JSON Key Path to Transformer and pick from the dropdown, then save. Each parser saves independently, so a transformer you selected but didn't save is the most common reason tool calls come back empty.
Click Ping Endpoint. The panel shows the parsed actual output and tools called next to the raw response.
Clone it for streaming
Open the three-dot menu on the connection you just built and choose Duplicate, then rename the copy to
OpenAI Responses (streaming). Change three things.General → AI App Endpoint: set Response Mode to
SSE Streaming.Body: add
"stream": true.{ "model": "gpt-5.4-mini", "input": golden.input, "stream": true, "tools": [ ... ] }Output parsing: an SSE connection needs to know which frame carries each value. The Responses API labels every frame with an
event:, running throughresponse.created,response.output_text.delta,response.output_item.done, and finallyresponse.completed, which carries the complete response object. Point both parsers at that last frame.Parser SSE Event Name Accumulate Events Extraction Actual output response.completedOff Transformer → openai_responses_outputTools called response.completedn/a Transformer → openai_responses_toolsPing it. You should see the same parsed values as the non-streaming connection, this time assembled from the stream.
Run an evaluation
Three inputs worth pinging before you point either connection at a full dataset:
Input What it checks What's the weather in San Francisco right now?One tool call is extracted, with argumentsparsed into aninputParametersobject.Compare the weather in San Francisco and Tokyo right now.Parallel tool calls. You should get two entries. Getting one means the tools parser is on response.output_item.done.Say pong three times.The text path still works. No tool call, and actual output comes back as the assistant's message. If the model answers in prose instead of calling the tool, add
"tool_choice": "required"to the body.Both connections now produce a
toolsCalledlist on every test case, which is what tool metrics score. Tool Correctness comparestoolsCalledagainst theexpectedToolson your golden, so set that on each golden:[{ "name": "get_weather", "inputParameters": { "city": "San Francisco" } }]Add the metric to a metric collection, then run a single-turn evaluation with each connection selected as the output generation method. Same model, same tools, one streamed and one not, so the two test runs are directly comparable.
Troubleshooting
| Ping error | Cause |
|---|---|
Tools called comes back null | The parser is still set to JSON Key Path, or the transformer was selected but not saved. Each parser saves on its own. |
Invalid Tools Called Transformation | The transformer raised. Usually the list-of-frames shape on a streaming ping, so keep the isinstance(data, list) branch. |
Invalid Tools Called Return Type | The return value isn't a list of ToolCall. Every entry needs a string name, and inputParameters has to be an object. |
Invalid Actual Output Return Type | The actual output transformer returned None or something that isn't a string. |
Empty streaming response | No frame matched the actual output parser, or the model returned tool calls only. Use the fallback in openai_responses_output. |
No data received from streaming response | "stream": true is missing from the body, so OpenAI replied with a single JSON response instead of an event stream. |
401 or 403 | The Authorization header is missing or malformed, or the key can't reach that model. |
Next Steps
AI Connections
Endpoints, payloads, output parsing, and headers in full.
Streaming
How SSE event names, key paths, and accumulate mode work together.
Transformers
Write, test, and manage the Python functions that reshape responses.
Authorization
Pull the API key from a secrets manager instead of a static header.
Last updated on