Launch Week 02 wrapped — explore all five launches

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": true in the body. The response arrives as Server-Sent Events, and the tool calls land in the final response.completed frame.

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:

  1. Tool calls sit inside output alongside everything else. output is a mixed array of reasoning, message, and function_call items, ordered however the model produced them. There's no fixed index to point a key path at.
  2. Arguments arrive as a JSON string, "{\"city\":\"Hong Kong\"}", not an object. inputParameters has to be a real object.
  3. The field names differ. The API calls it arguments. A ToolCall calls it inputParameters.

A transformer closes all three gaps. You write two of them once, under Project SettingsTransformers, and both connections share them.

Build It

  1. Write the tools called transformer

    Go to Project SettingsTransformersNew 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_called

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

  2. 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)
  3. Create the non-streaming connection

    Go to Project SettingsAI ConnectionsNew AI Connection, name it OpenAI Responses (non-streaming), and fill in:

    General → AI App Endpoint

    FieldValue
    Endpointhttps://api.openai.com/v1/responses
    Response ModeHTTP Response

    Running your own Responses-compatible endpoint? Put its URL here instead. Every other step in this guide stays the same.

    Headers

    KeyValue
    AuthorizationBearer sk-...
    Content-Typeapplication/json

    Body, in JSON payload mode. Type golden.input unquoted; 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, and parameters sit at the top level, not nested under a function key the way Chat Completions does it. With "strict": true, every key in properties also has to appear in required, and additionalProperties has to be false.

    Output parsing

    ParserSetting
    Actual outputTransformer → openai_responses_output
    Tools calledTransformer → openai_responses_tools

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

  4. 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 through response.created, response.output_text.delta, response.output_item.done, and finally response.completed, which carries the complete response object. Point both parsers at that last frame.

    ParserSSE Event NameAccumulate EventsExtraction
    Actual outputresponse.completedOffTransformer → openai_responses_output
    Tools calledresponse.completedn/aTransformer → openai_responses_tools

    Ping it. You should see the same parsed values as the non-streaming connection, this time assembled from the stream.

  5. Run an evaluation

    Three inputs worth pinging before you point either connection at a full dataset:

    InputWhat it checks
    What's the weather in San Francisco right now?One tool call is extracted, with arguments parsed into an inputParameters object.
    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 toolsCalled list on every test case, which is what tool metrics score. Tool Correctness compares toolsCalled against the expectedTools on 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 errorCause
Tools called comes back nullThe 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 TransformationThe transformer raised. Usually the list-of-frames shape on a streaming ping, so keep the isinstance(data, list) branch.
Invalid Tools Called Return TypeThe 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 TypeThe actual output transformer returned None or something that isn't a string.
Empty streaming responseNo 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 403The Authorization header is missing or malformed, or the key can't reach that model.

Next Steps

Scaling beyond prototype?For teams evaluating Confident AI in productionTalk to us

Last updated on

Built byConfident AI