# OpenAI Responses
The OpenAI Responses API streams model output as typed events over HTTP. AI Transport's ResponsesCodec encodes that stream onto an Ably channel, so the same server code feeds durable sessions instead of an ephemeral HTTP response.
The [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) is OpenAI's interface for calling a model and getting back a stream of typed events: output text, reasoning, refusals, function-call arguments, and lifecycle events. AI Transport writes that stream to an Ably channel, so reconnects, multi-device, and bidirectional control come for free.
The `ResponsesCodec` from `@ably/ai-transport/openai` passes each raw Responses event through to the channel and reassembles the conversation on the client. There is no OpenAI-specific transport, so you use the generic [`createAgentSession`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md) from `@ably/ai-transport` and pass it the codec.
To build a working app with this codec, see [Get started with OpenAI](https://ably.com/docs/ai-transport/getting-started/openai.md).
## What the OpenAI Responses API brings
The `openai` npm package owns the model call and the typed event model:
| Capability | Description |
| --- | --- |
| Responses streaming | `client.responses.create({ stream: true })` returns a stream of `ResponseStreamEvent`s: output-text deltas, reasoning, refusals, function-call arguments, and item lifecycle events. |
| Server-side tools | You advertise function tools on the request. The model emits function calls, you run them, and you feed the outputs back on the next Responses API call. |
| Reasoning models | Reasoning items carry the model's summarised thinking, and `encrypted_content` for the no-store, zero-data-retention round trip. |
| Round-trippable items | Each output item the codec stores is also valid Responses API input, so the conversation feeds the next turn without conversion. |
| Typed SDK | The official SDK owns auth, the HTTP call, streaming, and the Responses type definitions. |
## What AI Transport adds
AI Transport adds to the Responses API, writing the model stream to a durable session that outlives any single connection:
| Capability | Description |
| --- | --- |
| Durable sessions | Tokens flow through an Ably channel that outlives any single connection. A client reconnects and resumes from where it left off. |
| Multi-device sync | Every device subscribed to the session sees the same conversation in realtime. |
| Bidirectional control | Cancel, steer, and interrupt the agent from any client. No separate control channel. |
| Active run tracking | `view.runs()` exposes which clients have Runs streaming and which Runs are in progress. |
| Conversation branching | Edit and regenerate create forks in the conversation tree, not destructive replacements. |
| Approval gates that reach the user anywhere | Pending tool approvals persist on the session until someone acts on them. |
| History and replay | Load the full conversation on reconnect, page refresh, or new device join. |
| Token compaction | Reconnecting clients receive accumulated responses, not a replay of every token. |
## Where they connect
On the server, AI Transport swaps the HTTP response sink for `Run.pipe()`. Without it, the OpenAI SDK gives you no helper for forwarding a Responses stream over HTTP, so you serialise each event into a Server-Sent Events response by hand:
### Javascript
```
const encoder = new TextEncoder();
return new Response(
new ReadableStream({
async start(controller) {
for await (const event of stream) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
}
controller.close();
},
}),
{ headers: { 'Content-Type': 'text/event-stream' } },
);
```
With AI Transport, a raw `ResponseStreamEvent` is already valid codec output and `run.pipe` takes the SDK's async iterable directly, so the same stream reaches a durable session in two lines, and every subscribed client resumes, syncs, and cancels it:
### Javascript
```
const { reason } = await run.pipe(stream);
await run.end({ reason });
```
The integration has three pieces:
1. The `ResponsesCodec` from `@ably/ai-transport/openai` encodes each `ResponseStreamEvent` as Ably messages and reassembles them into `OpenAIMessage`s on the client. It streams assistant text, refusals, reasoning, and function-call arguments, and repairs a client that joins a stream mid-way. The codec passes the raw events through, so the wire tracks OpenAI's own event model.
2. `createAgentSession({ client, channelName, codec: ResponsesCodec })` from `@ably/ai-transport` constructs the agent session bound to the channel from the invocation.
3. `run.pipe()` reads the model's event stream, encodes each event, and publishes the resulting Ably messages. `run.abortSignal` wires cancellation through from the client to the Responses API request.
To feed the next turn, `toResponsesInput` flattens the conversation read from `run.view` back into the Responses `input` array. Each stored `OpenAIMessage` already holds valid Responses input items, so the flatten needs no conversion:
### Javascript
```
import { toResponsesInput } from '@ably/ai-transport/openai';
while (run.view.hasOlder()) {
await run.view.loadOlder();
}
const input = toResponsesInput(run.view.getMessages().map(({ message }) => message));
```
## Run server-side tools
A Responses stream never carries a function call's *output*. OpenAI surfaces tool output only as model input on the next turn, so the codec adds an event of its own, `function_call_output`, for the agent to publish after it runs a tool.
Server-executed tools do not suspend the Run. The agent runs an agentic loop: call the Responses API, and if the model emits function calls, run them, append the model's output items and the tool outputs to the input, and call it again. The loop continues until the model produces a reply with no tool calls. Each unit of work publishes under its own `run.pipe`, so a Run that calls one tool produces three messages: the turn that emitted the calls, the tool outputs, and the final text turn.
For reasoning models, the loop must re-append the whole turn's output items, including the reasoning items that preceded a function call, since reasoning models expect that reasoning to travel with the call on the next request. The [runnable demo](https://github.com/ably/ably-ai-transport-js/tree/main/demo/openai/react/use-client-session) implements the full loop.
## Run client-side tools and approvals
The codec also carries the client-driven half of tool calling: a client executes a tool in the browser and publishes the result, reports a tool failure, or answers a human approval prompt. Gating a call on a human decision needs the codec's second added event, `tool-approval-request`, which the Responses API has no equivalent for. The suspend and resume mechanics belong to the transport rather than the codec. The agent calls `run.suspend()` to wait for a client, the client publishes its input on the same `runId`, and a continuation resumes the Run.
`ResponsesCodec` exposes the full well-known factory set. You address each client input to the assistant message that holds the `function_call`, and key each payload by the OpenAI snake_case `call_id`:
### Javascript
```
import { ResponsesCodec } from '@ably/ai-transport/openai';
// A client-run tool succeeded.
await view.send(ResponsesCodec.createToolResult(codecMessageId, { call_id, output }), { runId });
// A client-run tool failed. The message becomes the output the model sees next turn.
await view.send(ResponsesCodec.createToolResultError(codecMessageId, { call_id, message }), { runId });
// A user approved or denied a gated tool.
await view.send(ResponsesCodec.createToolApprovalResponse(codecMessageId, { call_id, approved, reason }), { runId });
```
The Responses `function_call_output` item has no field for an approval decision or an error, so the codec holds that render-only state out of band on `OpenAIMessage.toolCallStates`, a map keyed by `call_id`. `toResponsesInput` never reads that state, so it cannot reach the model.
### Publish an approval request on the call's own message
To gate a tool on a human decision, the agent publishes the codec's own `tool-approval-request` output, carrying the `call_id`, the tool name, and the arguments so a client can render the prompt without the streamed `function_call`.
The agent publishes the request as the tail of the model turn's own `run.pipe`, so it lands on the same `codec-message-id` as the `function_call` it gates. The pending approval state, the client's decision, and the `function_call` then fold onto one message. Published as a separate message, the request strands its pending state on a message the client's response never amends, so the approval prompt never resolves and the agent never sees the call as approved.
#### Javascript
```
// Agent: the model turn, then an approval request per gated call, on one pipe.
async function* modelTurnWithGateRequests(input, signal, turn) {
yield* modelTurnStream(input, signal, turn);
for (const call of turn.calls) {
if (needsApproval(call.name)) {
yield { type: 'tool-approval-request', call_id: call.call_id, name: call.name, arguments: call.arguments };
}
}
}
await run.pipe(modelTurnWithGateRequests(input, run.abortSignal, turn));
```
### Wait for the run to suspend, then wake the agent
The client waits until the run reports `suspended` before publishing. One model turn can emit a server tool and a client tool on the same message, and resuming while the run is still active races the run's own output, whose server-tool result has not folded in yet. The provider then rejects the resumed request for the missing output. The run flips to `suspended` once the agent pauses it awaiting client input.
The client answers every open call before waking the agent. The model input must carry a matching output for every open `function_call`, so a turn that emits two gated calls needs both answers before either wakes the agent. The client reads the outstanding calls with `unansweredCalls`. Its own resolution is wire-only, since the transport skips the optimistic fold for an input targeting an existing message, so the client tracks the `call_id`s it has answered instead of waiting for them to appear in the view.
Publishing an input resumes nothing on its own. As everywhere else in the core SDK, the client publishes and then POSTs the invocation to wake the agent, in that order: the agent reads the conversation off the channel, so the resolution has to be there before the POST lands.
#### Javascript
```
import { unansweredCalls } from '@ably/ai-transport/openai';
const target = view.runOf(codecMessageId);
if (target?.status !== 'suspended') return;
answered.add(call_id);
const runMessages = view.messages
.filter((entry) => view.runOf(entry.codecMessageId)?.runId === target.runId)
.map((entry) => entry.message);
const answeredTheLastCall = unansweredCalls(runMessages).every((call) => answered.has(call.call_id));
const run = await view.send([input], { runId: target.runId });
if (answeredTheLastCall) await wakeAgent(run);
```
### Run an approved call on resume
An approval records the user's decision without producing the tool's output, so when the user approves a gated call the conversation holds a `function_call` with no `function_call_output`. The agent runs the tool server-side on resume, before the next model turn. It reads those calls with `approvedUnexecutedCalls`, publishes each output as its own message, and feeds them back into the input:
#### Javascript
```
import { approvedUnexecutedCalls } from '@ably/ai-transport/openai';
const approved = approvedUnexecutedCalls(priorMessages);
if (approved.length > 0) {
const { items, events } = runToolCalls(approved);
await run.pipe(outputStream(events));
input.push(...items);
}
```
A denial needs no server execution, because the codec resolves it with a rejection `function_call_output` on the client. The run is still suspended, so the client must wake it to continue.
The third correlation reader, `resolvedCallIds`, returns the `call_id`s that already carry an output, which a renderer uses to skip the calls it has already shown an answer for.
## Scope and trade-offs
The `ResponsesCodec` covers the shapes the Responses API streams: assistant text, refusals, reasoning (summary and raw), and server-executed function calls. It also covers the client-driven tool surface: client-executed tools, tool failures, and human approvals. Hosted tools (web and file search, code interpreter, image generation, MCP, custom tools) and audio are not yet supported.
The codec's event inventory is total, so an event outside it throws at the encoder instead of being dropped silently. An agent that enables a hosted tool must filter that tool's events, and the `output_text` annotations those tools cite, out of the stream before piping it.
The codec transmits the raw Responses events rather than a normalised abstraction. The wire therefore tracks OpenAI's own event model, which keeps stored items round-trippable to the Responses API but ties a conversation to the Responses shape. To integrate a different model provider behind one abstraction, use [Vercel AI SDK Core](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-core.md) instead.
## Read next
- [Get started with OpenAI](https://ably.com/docs/ai-transport/getting-started/openai.md): build a working app.
- [ResponsesCodec reference](https://ably.com/docs/ai-transport/api/javascript/openai/codec.md): the codec's methods, tool payloads, and types.
- [Conversation helpers reference](https://ably.com/docs/ai-transport/api/javascript/openai/conversation-helpers.md): `toResponsesInput` and the correlation readers.
- [Codec architecture](https://ably.com/docs/ai-transport/internals/codec-architecture.md): how a codec translates a framework's events into Ably messages.
- [Tool calling](https://ably.com/docs/ai-transport/features/tool-calling.md): the tool-calling model across the SDK.
- [Agent session API reference](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md): the full API surface for the agent session.
## Related Topics
- [Vercel AI SDK UI](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-ui.md): How Ably AI Transport integrates with Vercel AI SDK UI (@ai-sdk/react) to add durable sessions and multi-device sync to useChat.
- [Vercel AI SDK Core](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-core.md): How Ably AI Transport integrates with Vercel AI SDK Core (the `ai` library) on the server. Codec, streamText, and the UI message stream.
- [Vercel WDK](https://ably.com/docs/ai-transport/frameworks/vercel-wdk.md): How Ably AI Transport composes with Vercel Workflow Development Kit. Open the run and each model call as their own WDK steps, WDK step ids as AI Transport step ids, retries supersede on the session, and cancels route through Ably rather than workflow signals.
- [Temporal](https://ably.com/docs/ai-transport/frameworks/temporal.md): How Ably AI Transport composes with Temporal. One activity per step, activity ids as stepIds, retry supersedes on the session, cancels routed through Ably rather than Temporal signals.
## Documentation Index
To discover additional Ably documentation:
1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages.
2. Identify relevant URLs from that index.
3. Fetch target pages as needed.
Avoid using assumed or outdated documentation paths.