Глава 21

OpenAI: Responses API и function_call_output

Для новых проектов OpenAI рекомендует Responses API, и адаптер строится вокруг его модели данных. Ответ - это массив типизированных output items; tool calls находятся среди них, аргументы приходят JSON-строкой, а результат возвращается как function_call_output с тем же call_id. Понять это один раз проще, чем потом отлаживать "почему модель не видит результат".

Начинается адаптер с клиента и перевода нашего определения tool в форму Responses.

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
  };
}

Первый ход отправляет сообщение и tools, а состояние продолжения - это 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
  });
}

Продолжение после tools возвращает результаты как function_call_output по 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
  });
}

И нормализация ответа сводит output items к нашему единому 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 };
}

Одна деталь про хранение, о которую легко споткнуться.

Storage и privacy. Responses хранятся по умолчанию. Если политика требует store: false, используйте stateless continuation и передавайте нужные output items, включая reasoning items, вместе с tool outputs. Не смешивайте два режима случайно.

OpenAI можно взять и на прямом API, и на готовом harness. Разберём, когда второй выгоднее.

Ссылки