OpenAI Responses API

The Responses API uses event: + data: SSE format over HTTP, and is also available over WebSocket. aimock supports both transports with the same fixtures.

Endpoints

Method Path Format
POST /v1/responses HTTP SSE (event: + data:)
WS /v1/responses WebSocket JSON messages

Unit Test: HTTP SSE Text Response

responses-text.test.ts ts
import { createServer, type ServerInstance } from "@copilotkit/aimock/server";

const instance = await createServer([
  { match: { userMessage: "hello" }, response: { content: "Hi there!" } }
]);

const res = await post(`${instance.url}/v1/responses`, {
  model: "gpt-4o",
  input: [{ role: "user", content: "hello" }],
});

// Parse event: + data: SSE format
const events = res.body.split("\n\n")
  .filter(b => b.includes("event: ") && b.includes("data: "))
  .map(b => ({
    type: b.match(/^event: (.+)$/m)[1],
    data: JSON.parse(b.match(/^data: (.+)$/m)[1]),
  }));

const types = events.map(e => e.type);
expect(types).toContain("response.created");
expect(types).toContain("response.output_text.delta");
expect(types).toContain("response.completed");

Unit Test: Tool Call Response

responses-tools.test.ts ts
const instance = await createServer([
  {
    match: { userMessage: "weather" },
    response: {
      toolCalls: [{ name: "get_weather", arguments: JSON.stringify({ city: "NYC" }) }]
    }
  }
]);

const res = await post(`${instance.url}/v1/responses`, {
  model: "gpt-4o",
  input: [{ role: "user", content: "what is the weather?" }],
  stream: true,
});

const events = parseTypedSSE(res.body);
const types = events.map(e => e.type);
expect(types).toContain("response.function_call_arguments.delta");
expect(types).toContain("response.output_item.done");

SSE Event Sequence

The Responses API uses typed events. A text response produces this sequence:

  1. response.created
  2. response.in_progress
  3. response.output_item.added
  4. response.content_part.added
  5. response.output_text.delta (one per chunk)
  6. response.output_text.done
  7. response.content_part.done
  8. response.output_item.done
  9. response.completed

Function call responses follow the same pattern but use response.function_call_arguments.delta and response.function_call_arguments.done events. Custom tool calls use response.custom_tool_call_input.delta and response.custom_tool_call_input.done — see Namespaced and custom tool calls.

The same fixtures work for both HTTP SSE and WebSocket transports. See the WebSocket APIs page for WebSocket-specific details.

Ordered blocks (tool-first)

A combined content + toolCalls fixture accepts an optional blocks array to control stream order — see Ordered blocks. The Responses API has full support: output items (message, function_call or custom_tool_call) are assigned output_index in array order, so a tool call can precede the message and SDKs honor the ordering.

Namespaced and custom tool calls

The Responses API has two more tool-call shapes. aimock emits both over HTTP (streaming and non-streaming) and over WebSocket:

The responsesTools option

By default (responsesTools: "legacy") aimock matches, counts, journals and records Responses requests exactly as releases up to 1.44.0 did. Turn on the new behavior once per server with responsesTools: "extended" (programmatic) or --responses-tools extended (the llmock and aimock bins), or llm.responsesTools: "extended" in an aimock --config file. Any other option or config value is ignored with a warning and the server runs in legacy mode, as 1.44.0 ignored it; an invalid --responses-tools flag value exits 1. The mode is fixed when the server starts: changing responsesTools on the options object after createServer or LLMock.start() is unsupported. With it:

Codex CLI needs the option: its apply_patch is a custom tool and its MCP tools are namespaced.

fixtures/codex.json json
{
  "fixtures": [
    {
      "match": {
        "userMessage": "Look up aimock",
        "toolName": "search",
        "toolNamespace": "mcp__docs",
        "hasToolResult": false
      },
      "response": {
        "toolCalls": [
          { "name": "search", "namespace": "mcp__docs", "arguments": { "q": "aimock" } }
        ]
      }
    },
    {
      "match": { "userMessage": "add hello.txt", "hasToolResult": false },
      "response": {
        "toolCalls": [],
        "customToolCalls": [
          {
            "name": "apply_patch",
            "input": "*** Begin Patch\n*** Add File: hello.txt\n+hello\n*** End Patch\n"
          }
        ]
      }
    },
    {
      "match": { "hasToolResult": true },
      "response": { "content": "done" }
    }
  ]
}

mcp__docs is the namespace Codex gives an MCP server named docs. Each tool-call fixture is gated with hasToolResult: false and the last fixture answers every follow-up, so each call is made once and the agent stops. See the Codex recipe below.

A toolCalls entry is always a function call. It may carry namespace (emitted only with responsesTools: "extended"); any type or input on it is ignored, as in earlier releases. Custom tool calls go in customToolCalls: each entry takes name, free-text input (never parsed or stringified), and an optional id and namespace. They are emitted after the function calls of toolCalls; a turn with only custom calls sets "toolCalls": []. To place custom calls anywhere among text and function calls, use responsesBlocks: the same blocks as blocks plus a customToolCall block (name, input, optional id and namespace). A fixture may not set both blocks and responsesBlocks.

Event sequences

A namespaced function call streams exactly like any function call. The only change is namespace on the function_call item in response.output_item.added, response.output_item.done and response.completed. A call without a namespace has no namespace key, so existing fixtures produce the same output as before.

A custom tool call at output_index k streams as:

  1. response.output_item.added: a custom_tool_call item with id ctc_…, call_id, name, optional namespace, input: "" and status: "in_progress"
  2. response.custom_tool_call_input.delta, one per chunk (none when input is empty)
  3. response.custom_tool_call_input.done with the full input
  4. response.output_item.done: the same item with the full input and status: "completed"

A non-streaming response has the completed item in output. The call_id is the fixture's id when set.

Errors

Only the Responses API can serve a custom tool call. Every other provider endpoint (Chat Completions, Anthropic Messages, Gemini, Gemini Interactions, Bedrock, Ollama, Cohere, Realtime, Gemini Live) rejects a fixture with a non-empty customToolCalls, or with a customToolCall block in responsesBlocks, before any content, with the code aimock_unsupported_tool_call (Ollama /api/generate keeps its HTTP 400 for every tool-call fixture). The message names the tool and the API, for example aimock: fixture tool call "apply_patch" is a custom tool call, which only the OpenAI Responses API supports; this request arrived on OpenAI Chat Completions. A responsesBlocks without custom calls is served as blocks there. Those endpoints ignore namespace and send the bare tool name.

Fixture validation (aimock validate, --validate-on-load, addFixturesFromJSON(), POST /__aimock/fixtures) checks customToolCalls and responsesBlocks. A fixture that is not load-validated (a programmatic or factory fixture, or a file loaded without validation) is checked per request instead: a malformed custom call fails the Responses request before any content with the code aimock_invalid_fixture_tool_call and a message such as Invalid fixture tool call: "input" must be a string for a custom tool call (customToolCalls[0]), recorded in the journal entry. Fixtures that releases up to 1.44.0 accepted get no new validation finding and no new error.

Both codes are carried in the provider's error shape:

Matching

With responsesTools: "extended", aimock flattens the request's tools before matching: tools inside a namespace tool, custom tools (in req.customTools), and the tools of additional_tools (which Codex's responses-lite models send in place of tools) and tool_search_output input items are all visible. By default only top-level function tools are, as before. In either mode a malformed nested tool item (a namespace tool without a non-empty string name or with tools that is not an array, a null entry, or an input item whose tools is not an array) is dropped, as earlier releases ignored those items; a top-level tools that is not an array or holds null is still an HTTP 400.

Codex recipe

Start aimock with --responses-tools extended. Gate the turn-0 fixture with hasToolResult: false and the follow-up with hasToolResult: true. Codex sends the same user prompt and the same tools on the follow-up, so without the turn-0 gate the turn-0 fixture matches again and the agent loops. A fixture that matches only on toolName or toolNamespace has the same problem: the tool is offered on every request, so that fixture matches every request before any later fixture gets a chance.

fixtures/apply-patch.json json
{
  "fixtures": [
    {
      "match": { "userMessage": "Create hello.txt", "hasToolResult": false },
      "response": {
        "toolCalls": [],
        "customToolCalls": [
          {
            "name": "apply_patch",
            "input": "*** Begin Patch\n*** Add File: hello.txt\n+hello from aimock\n*** End Patch\n"
          }
        ]
      }
    },
    {
      "match": { "userMessage": "Create hello.txt", "hasToolResult": true },
      "response": { "content": "done" }
    }
  ]
}

Codex configuration notes: