1335 단어
7 분
OpenAI Responses API SSE Reversing Notes
2026-06-20
태그 없음

OpenAI Responses API — SSE Streaming Internals#

Source: OpenAI Python SDK openai-python (Stainless-generated)
Scope: SSE (Server-Sent Events) only. WebSocket mode excluded.
Last updated: 2026-06-20


1. High-Level Architecture#

flowchart TB
subgraph User["User Code"]
A["client.responses.create(stream=True)"]
end
subgraph HTTP["HTTP POST /v1/responses"]
B["POST /v1/responses<br/>headers: text/event-stream<br/>body: { stream: true }"]
end
subgraph SSE["SSE Decoding"]
C["SSEDecoder.decode(line)"]
D["ServerSentEvent"]
E["construct_type(discriminator=type)"]
end
subgraph SDK["SDK Layer"]
F["ResponseStreamState<br/>(accumulate + handle)"]
G["ResponseStreamEvent[]"]
end
A --> B
B -->|chunked transfer| C
C -->|line by line| D
D -->|JSON dict| E
E -->|typed event| F
F -->|yields| G

or via the convenience API:

with client.responses.stream(input="...", model="gpt-4o") as stream:
for event in stream:
...

which uses ResponseStreamManagerResponseStream internally.


2. SSE Wire Format#

Unlike the Assistants API (which uses named SSE event: fields like thread.message.delta), the Responses API embeds the event type directly in the JSON payload as a type field.

Each SSE chunk looks like:

data: {"type":"response.created","response":{...},"sequence_number":0}
data: {"type":"response.in_progress","response":{...},"sequence_number":1}
data: {"type":"response.output_item.added","output_index":0,"item":{...},"sequence_number":2}
data: {"type":"response.content_part.added","output_index":0,"item_id":"...","content_index":0,"part":{...},"sequence_number":3}
data: {"type":"response.output_text.delta","output_index":0,"item_id":"...","content_index":0,"delta":"Hello","sequence_number":4}
...
  • event: line is not set for Responses API SSE events.
  • The type field inside the JSON body is the discriminator.
  • Every event has a sequence_number: int for ordering.
  • The stream ends when the connection closes (no [DONE] sentinel for Responses; the [DONE] check in Stream._iter_events is only for legacy Chat Completions).

3. Full Event Catalog (53 types)#

3.1 Response Lifecycle Events#

#type literalClassFields
1response.createdResponseCreatedEventresponse: Response, sequence_number: int
2response.queuedResponseQueuedEventresponse: Response, sequence_number: int
3response.in_progressResponseInProgressEventresponse: Response, sequence_number: int
4response.completedResponseCompletedEventresponse: Response, sequence_number: int
5response.failedResponseFailedEventresponse: Response, sequence_number: int
6response.incompleteResponseIncompleteEventresponse: Response, sequence_number: int
7errorResponseErrorEventcode: Optional[str], message: str, param: Optional[str], sequence_number: int

3.2 Output Items — Structural#

#type literalClassFields
8response.output_item.addedResponseOutputItemAddedEventitem: ResponseOutputItem, output_index: int, sequence_number: int
9response.output_item.doneResponseOutputItemDoneEventitem: ResponseOutputItem, output_index: int, sequence_number: int
10response.content_part.addedResponseContentPartAddedEventpart: Part, content_index: int, item_id: str, output_index: int, sequence_number: int
11response.content_part.doneResponseContentPartDoneEventpart: Part, content_index: int, item_id: str, output_index: int, sequence_number: int

Part is a discriminated union of:

  • ResponseOutputText (type=output_text)
  • ResponseOutputRefusal (type=refusal)
  • PartReasoningText (type=reasoning_text)

3.3 Text Content#

#type literalClassFields
12response.output_text.deltaResponseTextDeltaEventdelta: str, content_index: int, item_id: str, output_index: int, logprobs: List[Logprob], sequence_number: int
13response.output_text.doneResponseTextDoneEventtext: str, content_index: int, item_id: str, output_index: int, logprobs: List[Logprob], sequence_number: int
14response.output_text.annotation.addedResponseOutputTextAnnotationAddedEventannotation: object, annotation_index: int, content_index: int, item_id: str, output_index: int, sequence_number: int

logprob structure:

Logprob:
token: str
logprob: float
top_logprobs: Optional[List[LogprobTopLogprob]] # up to 20 alternatives
LogprobTopLogprob:
token: Optional[str]
logprob: Optional[float]

3.4 Refusal#

#type literalClassFields
15response.refusal.deltaResponseRefusalDeltaEventdelta: str, content_index: int, item_id: str, output_index: int, sequence_number: int
16response.refusal.doneResponseRefusalDoneEventrefusal: str, content_index: int, item_id: str, output_index: int, sequence_number: int

3.5 Function Calling#

#type literalClassFields
17response.function_call_arguments.deltaResponseFunctionCallArgumentsDeltaEventdelta: str, item_id: str, output_index: int, sequence_number: int
18response.function_call_arguments.doneResponseFunctionCallArgumentsDoneEventarguments: str, name: str, item_id: str, output_index: int, sequence_number: int

3.6 MCP (Model Context Protocol)#

#type literalClassFields
19response.mcp_call.in_progressResponseMcpCallInProgressEventitem_id: str, output_index: int, sequence_number: int
20response.mcp_call_arguments.deltaResponseMcpCallArgumentsDeltaEventdelta: str (JSON partial), item_id: str, output_index: int, sequence_number: int
21response.mcp_call_arguments.doneResponseMcpCallArgumentsDoneEventarguments: str (JSON full), item_id: str, output_index: int, sequence_number: int
22response.mcp_call.completedResponseMcpCallCompletedEventitem_id: str, output_index: int, sequence_number: int
23response.mcp_call.failedResponseMcpCallFailedEventitem_id: str, output_index: int, sequence_number: int
24response.mcp_list_tools.in_progressResponseMcpListToolsInProgressEventitem_id: str, output_index: int, sequence_number: int
25response.mcp_list_tools.completedResponseMcpListToolsCompletedEventitem_id: str, output_index: int, sequence_number: int
26response.mcp_list_tools.failedResponseMcpListToolsFailedEventitem_id: str, output_index: int, sequence_number: int

3.7 Audio#

#type literalClassFields
27response.audio.deltaResponseAudioDeltaEventdelta: str (Base64 audio bytes), sequence_number: int
28response.audio.doneResponseAudioDoneEventsequence_number: int
29response.audio.transcript.deltaResponseAudioTranscriptDeltaEventdelta: str, sequence_number: int
30response.audio.transcript.doneResponseAudioTranscriptDoneEventsequence_number: int

3.8 Web Search Tool#

#type literalClassFields
31response.web_search_call.in_progressResponseWebSearchCallInProgressEventitem_id: str, output_index: int, sequence_number: int
32response.web_search_call.searchingResponseWebSearchCallSearchingEventitem_id: str, output_index: int, sequence_number: int
33response.web_search_call.completedResponseWebSearchCallCompletedEventitem_id: str, output_index: int, sequence_number: int

3.9 File Search Tool#

#type literalClassFields
34response.file_search_call.in_progressResponseFileSearchCallInProgressEventitem_id: str, output_index: int, sequence_number: int
35response.file_search_call.searchingResponseFileSearchCallSearchingEventitem_id: str, output_index: int, sequence_number: int
36response.file_search_call.completedResponseFileSearchCallCompletedEventitem_id: str, output_index: int, sequence_number: int

3.10 Code Interpreter Tool#

#type literalClassFields
37response.code_interpreter_call.in_progressResponseCodeInterpreterCallInProgressEventitem_id: str, output_index: int, sequence_number: int
38response.code_interpreter_call.interpretingResponseCodeInterpreterCallInterpretingEventitem_id: str, output_index: int, sequence_number: int
39response.code_interpreter_call_code.deltaResponseCodeInterpreterCallCodeDeltaEventdelta: str, item_id: str, output_index: int, sequence_number: int
40response.code_interpreter_call_code.doneResponseCodeInterpreterCallCodeDoneEventcode: str, item_id: str, output_index: int, sequence_number: int
41response.code_interpreter_call.completedResponseCodeInterpreterCallCompletedEventitem_id: str, output_index: int, sequence_number: int

3.11 Reasoning#

#type literalClassFields
42response.reasoning_text.deltaResponseReasoningTextDeltaEventdelta: str, content_index: int, item_id: str, output_index: int, sequence_number: int
43response.reasoning_text.doneResponseReasoningTextDoneEventtext: str, content_index: int, item_id: str, output_index: int, sequence_number: int
44response.reasoning_summary_part.addedResponseReasoningSummaryPartAddedEventpart: Part (summary_text), summary_index: int, item_id: str, output_index: int, sequence_number: int
45response.reasoning_summary_part.doneResponseReasoningSummaryPartDoneEventpart: Part (summary_text), summary_index: int, item_id: str, output_index: int, sequence_number: int
46response.reasoning_summary_text.deltaResponseReasoningSummaryTextDeltaEventdelta: str, summary_index: int, item_id: str, output_index: int, sequence_number: int
47response.reasoning_summary_text.doneResponseReasoningSummaryTextDoneEventtext: str, summary_index: int, item_id: str, output_index: int, sequence_number: int

3.12 Image Generation#

#type literalClassFields
48response.image_generation_call.in_progressResponseImageGenCallInProgressEventitem_id: str, output_index: int, sequence_number: int
49response.image_generation_call.generatingResponseImageGenCallGeneratingEventitem_id: str, output_index: int, sequence_number: int
50response.image_generation_call.partial_imageResponseImageGenCallPartialImageEventpartial_image_b64: str, partial_image_index: int (0-based), item_id: str, output_index: int, sequence_number: int
51response.image_generation_call.completedResponseImageGenCallCompletedEventitem_id: str, output_index: int, sequence_number: int

3.13 Custom Tool Call#

#type literalClassFields
52response.custom_tool_call_input.deltaResponseCustomToolCallInputDeltaEventdelta: str, item_id: str, output_index: int, sequence_number: int
53response.custom_tool_call_input.doneResponseCustomToolCallInputDoneEventinput: str, item_id: str, output_index: int, sequence_number: int

4. SSE Parsing Pipeline (Wire to Typed Event)#

flowchart TB
subgraph HTTP["HTTP Response Stream"]
A["httpx.Response.iter_bytes()"]
end
subgraph SSE["SSE Decoder (WHATWG SSE)"]
B["SSEDecoder.decode(line)"]
C["ServerSentEvent<br/>.event - None for Responses<br/>.data - raw JSON string"]
end
subgraph DISPATCH["Stream Dispatcher"]
D{"sse.data starts with [DONE]?"}
E{"sse.event starts with thread.?"}
F["construct_type(discriminator=type)"]
end
subgraph OUT["Typed Event"]
G["ResponseCreatedEvent |<br/>ResponseTextDeltaEvent |<br/>... (52 more types)"]
end
A --> B
B --> C
C --> D
D -->|yes| H["break (legacy)"]
D -->|no| E
E -->|yes| I["thread. events (Assistants)"]
E -->|no| F
F --> G

Key decoder details#

  • SSEDecoder accumulates lines until a blank line (\n\n, \r\r, or \r\n\r\n).
  • Each SSE field (event:, data:, id:, retry:) is processed per spec.
  • For Responses, event: is absent → ServerSentEvent.event is None.
  • The raw JSON data: is deserialized and passed to construct_type().
  • ResponseStreamEvent is Annotated[Union[…], PropertyInfo(discriminator=“type”)], so construct_type examines the “type” key to dispatch to the correct subclass.

5. SDK Convenience API: ResponseStream / ResponseStreamState#

5.1 ResponseStreamManager (entry point)#

Usage:

with client.responses.stream(
input="Hello", model="gpt-4o"
) as stream: # returns ResponseStream[TextFormatT]
for event in stream:
...

The manager:

  1. Calls client.responses.create(stream=True, …) to get the raw Stream[ResponseStreamEvent].
  2. Wraps it in ResponseStream(raw_stream=…, text_format=…, input_tools=…).
  3. Exposes the stream as an iterator of ResponseStreamEvent[TextFormatT].

5.2 ResponseStreamResponseStreamState#

ResponseStream.__stream__() delegates to ResponseStreamState.handle_event(sse_event) for each raw event.

handle_event() produces 1 or 2 events per raw SSE event:

Raw SSE typeEmitted events
response.output_text.deltaResponseTextDeltaEvent (with snapshot field added — accumulated text so far)
response.output_text.doneResponseTextDoneEvent (with parsed field if text_format is set)
response.function_call_arguments.deltaResponseFunctionCallArgumentsDeltaEvent (with snapshot — accumulated args so far)
response.completedResponseCompletedEvent (with response being the full parsed ParsedResponse)
All othersPassed through as-is

5.3 accumulate_event() — State Machine#

ResponseStreamState maintains a __current_snapshot (partial ParsedResponse):

  • response.created → Creates initial snapshot from event.response.
  • response.output_item.added → Appends item to snapshot.output[] (handles function_call, message, and generic types).
  • response.content_part.added → Appends content part to the indexed output message.
  • response.output_text.delta → Concatenates delta to the matching output_text content’s text.
  • response.function_call_arguments.delta → Concatenates delta to the matching function call’s arguments.
  • response.completed → Stores the fully parsed response via parse_response() (also handles structured output parsing).

5.4 Convenience methods#

  • stream.get_final_response() → Consumes entire stream, returns the accumulated ParsedResponse.
  • stream.until_done() → Blocks until stream fully consumed.
  • stream.close() → Closes underlying httpx response.

5.5 ResponseStreamEvent[TextFormatT] (SDK wrapper types)#

These subclass or wrap the raw generated types with extra fields:

Raw typeSDK wrapperExtra fields
ResponseTextDeltaEventlib.streaming.responses.ResponseTextDeltaEventsnapshot: str (accumulated text)
ResponseTextDoneEventlib.streaming.responses.ResponseTextDoneEventparsed: Optional[TextFormatT]
ResponseFunctionCallArgumentsDeltaEventlib.streaming.responses.ResponseFunctionCallArgumentsDeltaEventsnapshot: str (accumulated args)
ResponseCompletedEventlib.streaming.responses.ResponseCompletedEventresponse: ParsedResponse[TextFormatT]

The composite ResponseStreamEvent type alias used by the stream is:

ResponseStreamEvent = Annotated[Union[
ResponseTextDeltaEvent, # SDK wrapper
ResponseTextDoneEvent, # SDK wrapper
ResponseFunctionCallArgumentsDeltaEvent, # SDK wrapper
ResponseCompletedEvent, # SDK wrapper
ResponseAudioDeltaEvent, # raw
... (all other raw events) # raw
], PropertyInfo(discriminator="type")]

6. Background Mode Streaming#

When background=True is passed to responses.stream():

  1. The API request includes “background”: true.
  2. The server immediately returns a response.created event with the response id.
  3. The stream stays open but yields events asynchronously as the background processing progresses.
  4. The stream can be interrupted and resumed: note the response_id from response.created, then later call responses.stream(response_id=id, starting_after=N) to resume from sequence number N.

Usage pattern:

with client.responses.stream(input="...", model="gpt-4o", background=True) as stream:
for event in stream:
if event.type == "response.created":
id = event.response.id
if event.sequence_number == 10:
break
# Later...
with client.responses.stream(response_id=id, starting_after=10) as stream:
for event in stream:
...
response = stream.get_final_response()

7. Data Flow Summary#

flowchart TB
subgraph CLIENT["Client"]
A["HTTP POST /v1/responses<br/>{ stream: true }"]
end
subgraph PROXY["HTTP Transport"]
B["Content-Type: text/event-stream<br/>chunked transfer"]
end
subgraph PARSE["SSE Parser"]
C["SSEDecoder<br/>- buffers \n\n chunks<br/>- parses event:/data:"]
end
subgraph DISP["Type Dispatch"]
D["construct_type()<br/>via type discriminator"]
end
subgraph ACC["Accumulator"]
E["ResponseStreamState<br/>- maintains snapshot<br/>- adds snapshot/parsed"]
end
subgraph USER["User"]
F["for event in stream:"]
end
A --> B
B --> C
C --> D
D --> E
E --> F

8. Wire-Level Example (Synthetic)#

data: {"type":"response.created","response":{"id":"resp_abc123","object":"response","status":"queued","created_at":1718000000,"model":"gpt-4o","output":[],"tools":[],"tool_choice":"auto","parallel_tool_calls":true,"top_p":1,"temperature":1},"sequence_number":0}
data: {"type":"response.queued","response":{"id":"resp_abc123",...},"sequence_number":1}
data: {"type":"response.in_progress","response":{"id":"resp_abc123","status":"in_progress",...},"sequence_number":2}
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"item_1","type":"message","role":"assistant","content":[],"status":"in_progress"},"sequence_number":3}
data: {"type":"response.content_part.added","output_index":0,"item_id":"item_1","content_index":0,"part":{"type":"output_text","text":"","annotations":[]},"sequence_number":4}
data: {"type":"response.output_text.delta","output_index":0,"item_id":"item_1","content_index":0,"delta":"Hello","sequence_number":5,"logprobs":[{"token":"Hello","logprob":-0.01}]}
data: {"type":"response.output_text.delta","output_index":0,"item_id":"item_1","content_index":0,"delta":" world","sequence_number":6,"logprobs":[...]}
data: {"type":"response.output_text.done","output_index":0,"item_id":"item_1","content_index":0,"text":"Hello world","sequence_number":7,"logprobs":[...]}
data: {"type":"response.content_part.done","output_index":0,"item_id":"item_1","content_index":0,"part":{"type":"output_text","text":"Hello world","annotations":[]},"sequence_number":8}
data: {"type":"response.output_item.done","output_index":0,"item":{"id":"item_1","type":"message","role":"assistant","content":[{"type":"output_text","text":"Hello world","annotations":[]}],"status":"completed"},"sequence_number":9}
data: {"type":"response.completed","response":{"id":"resp_abc123","status":"completed","output":[{"id":"item_1",...}],...},"sequence_number":10}

9. Implementation Reference (Key Files in SDK)#

FilePurpose
src/openai/_streaming.pyStream[T], AsyncStream[T], SSEDecoder, SSEBytesDecoder, ServerSentEvent
src/openai/_response.pyAPIResponse, _parse() → constructs Stream[T]
src/openai/_base_client.pyrequest(), _process_response(), _process_response_data(), _make_sse_decoder(), make_request_options()
src/openai/types/responses/response_stream_event.pyResponseStreamEvent discriminated union
src/openai/lib/streaming/responses/_responses.pyResponseStream, AsyncResponseStream, ResponseStreamManager, ResponseStreamState
src/openai/lib/streaming/responses/_events.pySDK-enhanced wrapper types + ResponseStreamEvent (lib version)
src/openai/resources/responses/responses.pyResponses.create(), Responses.stream(), WebSocket connection code

10. Event Taxonomy Diagram#

graph LR
subgraph LIFECYCLE["Response Lifecycle"]
RC["response.created"] --> RQ["response.queued"]
RQ --> RI["response.in_progress"]
RI --> RC2["response.completed"]
RI --> RF["response.failed"]
RI --> RINC["response.incomplete"]
end
subgraph CONTENT["Content Delta"]
OT["output_text.delta"] --> OTD["output_text.done"]
OTD --> OTA["output_text.annotation"]
RD["refusal.delta"] --> RD2["refusal.done"]
AD["audio.delta"] --> ADD["audio.done"]
ATD["audio.transcript.delta"] --> ATDD["audio.transcript.done"]
RT["reasoning_text.delta"] --> RTD["reasoning_text.done"]
RS["reasoning_summary_part<br/>reasoning_summary_text"]
end
subgraph STRUCT["Output Structure"]
OIA["output_item.added"] --> OID["output_item.done"]
CPA["content_part.added"] --> CPD["content_part.done"]
end
subgraph IMAGE["Image Generation"]
IGI["image_gen_call.in_progress"] --> IGG["image_gen_call.generating"]
IGG --> IGP["image_gen_call.partial_image"]
IGP --> IGC["image_gen_call.completed"]
end
subgraph TOOLS["Tool Calls"]
FC["function_call_arguments.delta/done"]
MCP["mcp_call.* (8 events)"]
WS["web_search_call.* (3 events)"]
FS["file_search_call.* (3 events)"]
CI["code_interpreter_call.* (5 events)"]
CT["custom_tool_call_input.delta/done"]
end
LIFECYCLE --> CONTENT
LIFECYCLE --> STRUCT
LIFECYCLE --> TOOLS
LIFECYCLE --> IMAGE

Pro tip: The SDK docstrings only document response.created. Use the type definitions from src/openai/types/responses/response__event.py as the canonical source. Every event has the type literal (matching the type field in the JSON), sequence_number: int, and event-specific fields.