Chapter 21

OpenAI: The Responses API and function_call_output

For new projects OpenAI recommends the Responses API, and the adapter is built around its data model. The response is an array of typed output items; tool calls are among them, arguments arrive as a JSON string, and the result is returned as a function_call_output with the same call_id. Understanding this once is easier than later debugging "why the model doesn't see the result."

The adapter starts with the client and the translation of our tool definition into the Responses shape.

TypeScript
import OpenAI from "openai";

const client = new OpenAI();
const model = process.env.OPENAI_MODEL ?? "gpt-5.6";

type OpenAIState = {
  previousResponseId: string;
  instructions: string;
  tools: ToolDefinition[];
};

function toOpenAITool(tool: ToolDefinition) {
  return {
    type: "function" as const,
    name: tool.name,
    description: tool.description,
    parameters: tool.parameters,
    strict: true
  };
}

The first move sends the message and tools, and the continuation state is the response.id.

TypeScript
async function start(input: AgentInput): Promise<ProviderTurn> {
  const response = await client.responses.create({
    model,
    instructions: input.instructions,
    input: input.message,
    tools: input.tools.map(toOpenAITool),
    parallel_tool_calls: true
  });

  return fromOpenAIResponse(response, {
    previousResponseId: response.id,
    instructions: input.instructions,
    tools: input.tools
  });
}

The continuation after tools returns the results as function_call_output keyed by previous_response_id.

TypeScript
async function resume(
  rawState: unknown,
  results: ToolResult[]
): Promise<ProviderTurn> {
  const state = openAIStateSchema.parse(rawState);
  const response = await client.responses.create({
    model,
    previous_response_id: state.previousResponseId,
    instructions: state.instructions,
    tools: state.tools.map(toOpenAITool),
    input: results.map(function toOutput(result) {
      return {
        type: "function_call_output" as const,
        call_id: result.callId,
        output: JSON.stringify({
          ok: !result.isError,
          value: result.output
        })
      };
    })
  });

  return fromOpenAIResponse(response, {
    ...state,
    previousResponseId: response.id
  });
}

And normalizing the response reduces the output items to our single ProviderTurn.

TypeScript
function fromOpenAIResponse(response: any, state: OpenAIState): ProviderTurn {
  const calls = response.output
    .filter(function isCall(item: any) {
      return item.type === "function_call";
    })
    .map(function normalize(item: any): ToolCall {
      return {
        id: item.call_id,
        name: item.name,
        arguments: JSON.parse(item.arguments)
      };
    });

  if (calls.length > 0) return { kind: "tool_calls", calls, state };
  return { kind: "text", text: response.output_text, state };
}

One detail about storage that's easy to trip on.

Storage and privacy. Responses are stored by default. If policy requires store: false, use a stateless continuation and pass the needed output items, including reasoning items, together with the tool outputs. Don't mix the two modes by accident.

OpenAI can be taken on both the direct API and a ready harness. Let's look at when the second pays off.

Links