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
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
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:
response.createdresponse.in_progressresponse.output_item.addedresponse.content_part.addedresponse.output_text.delta(one per chunk)response.output_text.doneresponse.content_part.doneresponse.output_item.doneresponse.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:
-
A
function_callwithnamespace. A tool offered inside a{"type": "namespace", "name": "...", "tools": [...]}tool is called with its namespace next to its name. Codex puts each MCP server's tools in one namespace and sub-agent tools in thecollaborationnamespace. -
A
custom_tool_call. A custom (freeform) tool ({"type": "custom", ...}, with an optional Lark or regex grammar) is called with free-textinput, not JSON arguments. Codex'sapply_patchis a custom tool.
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:
-
toolNameand predicates see the tools insidenamespacetools,customtools, and the tools ofadditional_toolsandtool_search_outputinput items; -
custom_tool_call/custom_tool_call_outputhistory counts as a tool round forhasToolResult,toolCallId,toolResultContainsandturnIndex; -
the
namespaceof atoolCallsentry or atoolCallblock is emitted; - record mode keeps namespaces and custom tool calls;
-
the keys
match.toolNamespace,customToolCallsandresponsesBlocksapply. By default they are ignored, as in earlier releases.
Codex CLI needs the option: its apply_patch is a custom tool and its MCP
tools are namespaced.
{
"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:
-
response.output_item.added: acustom_tool_callitem withidctc_…,call_id,name, optionalnamespace,input: ""andstatus: "in_progress" -
response.custom_tool_call_input.delta, one per chunk (none wheninputis empty) -
response.custom_tool_call_input.donewith the fullinput -
response.output_item.done: the same item with the fullinputandstatus: "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:
-
OpenAI Responses, OpenAI Chat Completions, Azure OpenAI and Cohere: HTTP 500 with
error.codein an OpenAI-style envelope. -
OpenRouter Chat Completions: HTTP 500 with the code in
error.metadata.reason. -
Anthropic Messages: HTTP 500 in Anthropic's error envelope (
error.typeapi_error), with the code inerror.code. - Gemini Interactions: HTTP 500 with the code in
error.code. -
Gemini and Vertex AI: HTTP 500 with a
google.rpc.ErrorInfoentry inerror.details. -
Bedrock: HTTP 500 with the code in
reason, next to__type. -
Ollama
/api/chat: HTTP 500 with acodemember next to the stringerror. -
OpenAI Responses over WebSocket: an
errorevent with the code inerror.code. -
Realtime:
response.createdand thenresponse.donewithstatus: "failed"and the code inresponse.status_details.error.code. -
Gemini Live: an error frame with
code13 and the samegoogle.rpc.ErrorInfodetail as Gemini.
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.
-
toolNamematches an offered tool by name. Other tool types (web_search,file_search,mcp, and so on) are not visible to matching. -
toolNamespacematches an offered tool's namespace exactly, withresponsesTools: "extended"(by default the key is ignored). Alone, it matches when any tool sits in that namespace. WithtoolName, one tool must carry both. There is no default namespace: a top-level tool has none. Chat Completions passes request tools through unchanged, so a top-levelnamespacefield that a client sends there also matches. -
With
responsesTools: "extended", apredicateseesnamespaceon each tool inreq.tools, custom tools inreq.customTools, history function calls with theirnamespace, and custom tool calls inmessage.custom_tool_calls.
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": [
{
"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:
-
Point a custom provider at aimock (
base_url = "http://127.0.0.1:4010/v1",wire_api = "responses") and use modelgpt-5.5. Codex's other bundled models send their tools inadditional_toolsand run in code mode, and an unknown model slug gets noapply_patchtool. -
Codex defers MCP tools behind
tool_search, which aimock does not mock. Setomit_tools_from = ["deferred"]on the MCP server so its tools are offered directly, in a namespace namedmcp__<server>(for examplemcp__docsfor[mcp_servers.docs], with no trailing__).toolNamespaceis an exact match, so if a match fails, read the exact values from the aimock journal: withresponsesTools: "extended"each entry'sbody.tools[]carriesnamespaceandfunction.name. Withcodex exec(approval policynever), also setdefault_tools_approval_mode = "approve"on that server, or Codex refuses the MCP call.