Input_message doesn't support role as assistant

Hi experts,

I recently found the input_message doesn’t support input_text as a type for role of assistant.

example:

curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-5-nano",
"input":[
        {
            "role": "assistant",
            "content": [
                { "type": "input_text", "text": "what is in this image?" }
            ],
            "type": "message"
        }
    ]
}'
{
  "error": {
    "message": "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.",
    "type": "invalid_request_error",
    "param": "input[0].content[0]",
    "code": "invalid_value"
  }
}

I remember it used to be supported. In the spec, it also shows supporting role as assistant, the only type we could choose for input message is input_text, I’m thinking if there is bug introduced recently for the field validation.

Could we remove the unnecessary check for the input_text type of assistant role
related thread:

https://community.openai.com/t/input-text-field-removed-from-response-api/1363365/6?u=key4ng

I think that is becasue for input_text you are supposed to use ‘developer” role ? From the API docss: A message input to the model with a role indicating instruction following hierarchy. Instructions given with the developer or system role take precedence over instructions given with the user role. Messages with the assistant role are presumed to have been generated by the model in previous interactions.

While I had already agreed that this is a bit too strict, there is actually a much simpler alternative syntax for just plain text:

response = client.responses.create(
    model="gpt-4.1-mini",
    input=[
        {
            "role": "assistant",
            "content":  "I am chattygpt" 
        },
        {
            "role": "user",
            "content": "chatty what?"
        }
    ]
)

print(response.output_text)

Depending on your use case, perhaps this might help to simplify things.

To put it simply: “assistant” role messages with content are type “output_text”.

This message type is meant to replay a previous response object also received with that type, and you would simulate that if not using an actual assistant turn as received.

Output is an array of many types, from reasoning (with both encrypted content and useless summary items), tool calls, and the final seen output. OpenAI has taken the basic production of tokens an AI produces, and made server-side parsing into objects, and a strict formatting and sequence is then required in replay.

Each type still takes client-side understanding and rewriting for placement back as input.

What follows is AI production from a lossless input of prepared and curated documentation and iterative turns re-collected, along with task instructions and understanding. This conversation input, BTW, looks like:

user: 181888 tokens
assistant: 6041 tokens
user: 645 tokens
assistant: reasoning + this text before review and edit


Below is reference you can use to understand better the stateless conversation replay with the OpenAI Responses API. It covers:

  • What streaming events to collect and how they assemble into response output items
  • What the completed response.output contains (object types)
  • What the API accepts in the next request’s input
  • A clear output-to-input mapping table that you can implement directly
  • Special handling for encrypted reasoning

The forum has limited width for the tabular data, so you may unfortunately need to scroll around or pick the large table view.


How to think about streaming collection

  • Stream events arrive as “start → deltas → done” for each content part or tool call. Only the “done” events contain the authoritative finalized string.
  • The final response.completed event contains a normalized output array that is equivalent to non-streaming mode. It’s the easiest single source to mirror when you build the next turn’s input for stateless conversation.
  • If you are using stateless mode (store=false or zero data retention), you must replay the entire conversation context as input in the correct order. If you use conversation:{id} and let OpenAI store state, you usually just send the new user message (no replay needed).

Table 1. Streaming event lifecycles → what to collect for the final output array

output kind (in response.completed.output) lifecycle events to watch what to collect into the item
Assistant message (text/refusal) response.output_item.added (message)
response.content_part.added (output_text or refusal)
response.output_text.delta → response.output_text.done
response.refusal.delta → response.refusal.done
response.content_part.done
response.output_item.done
Finalized assistant OutputMessage with content array of {type: output_text, text, annotations} and/or {type: refusal, refusal}.
Reasoning summary (preview) response.output_item.added (reasoning)
response.reasoning_summary_part.added
response.reasoning_summary_text.delta → response.reasoning_summary_text.done
response.reasoning_summary_part.done
response.output_item.done
Reasoning item with summary parts (summary_text). This is for display/debug; it’s not needed for model context replay.
Reasoning text (OSS only) response.content_part.added (reasoning_text)
response.reasoning_text.delta → response.reasoning_text.done
response.content_part.done
Reasoning item content parts with reasoning_text (plain text). You’ll use encrypted reasoning instead (see Table 5).
Function tool call (model-issued) response.output_item.added (function_call)
response.function_call_arguments.delta → response.function_call_arguments.done
response.output_item.done
Output item: {type:function_call, call_id, name, arguments}. You’ll include this item in next input (to preserve the dialog) and separately send your tool result as function_call_output.
Custom tool call (model-issued) response.output_item.added (custom_tool_call)
response.custom_tool_call_input.delta → response.custom_tool_call_input.done
response.output_item.done
Output item: {type: custom_tool_call, call_id, name, input}. Include this item in next input; separately send custom_tool_call_output with your tool’s output.
MCP tool call response.output_item.added (mcp_call)
response.mcp_call_arguments.delta → .done
response.mcp_call.in_progress → .completed/.failed
response.output_item.done
Output item: {type:mcp_call, server_label, name, arguments, output?, error?, status}. Include it to preserve context.
File search call response.output_item.added (file_search_call)
response.file_search_call.in_progress → .searching → .completed
response.output_item.done
Output item: {type:file_search_call, status, queries, results?}. Include it to preserve context.
Web search call response.output_item.added (web_search_call)
response.web_search_call.in_progress → .searching → .completed
response.output_item.done
Output item: {type:web_search_call, status, action:{search/open_page/find…}}. Include to preserve context.
Code interpreter call response.output_item.added (code_interpreter_call)
response.code_interpreter_call.in_progress → .interpreting
response.code_interpreter_call_code.delta → .done
response.code_interpreter_call.completed
response.output_item.done
Output item: {type:code_interpreter_call, status, container_id, code?, outputs?}. Include to preserve context.
Image generation call response.output_item.added (image_generation_call)
response.image_generation_call.in_progress → .generating → partial_image* → .completed
response.output_item.done
Output item: {type:image_generation_call, status, result?}. Include to preserve context.
Audio output (TTS style) response.audio.delta → response.audio.done
response.audio.transcript.delta → .done
Audio bytes and transcript are not typically re-sent. Will likely need a prior ID, like Chat Completions. If you want the transcript in context, add it yourself next turn as output_text.

Table 2. Output array (response.completed.output) — items you may see

output item.type description key fields to store needed for next-turn context?
message (assistant) Final assistant message content id, role=assistant, content of output_text/annotations or refusal Yes, include the assistant OutputMessage as-is.
reasoning Reasoning preview/text id, summary, content?, encrypted_content? Include only if you plan to supply encrypted_content back (see Table 5). Do not include summary in input.
function_call Model called your function id, call_id, name, arguments (JSON string) Yes (to preserve what model did). You will also send function_call_output with your tool’s result.
function_call_output Your function result (only appears if you sent it) id?, call_id, output (string or content list) Send from your app as input in the next turn (not created by the model). Include to preserve tool outputs.
custom_tool_call Model called your custom tool id, call_id, name, input Yes; also send custom_tool_call_output with your tool’s result.
custom_tool_call_output Your custom tool result (if you sent it) id?, call_id, output (string or content list) Send from your app as input (not created by model). Include to preserve tool outputs.
mcp_call Model invoked a remote MCP tool id, server_label, name, arguments, output?, error?, status Yes; include to preserve context.
mcp_list_tools List of tools on MCP server id, server_label, tools, error? Yes; if you want the model to remember the discovered tools.
mcp_approval_request MCP approval requested id, server_label, name, arguments Include if you want to preserve the approval interaction in context.
mcp_approval_response Human approval response (if you sent it) id?, approval_request_id, approve, reason? Send from your app as input; include later for context.
file_search_call Built-in file search call id, status, queries, results? Yes; include.
web_search_call Built-in web search call id, status, action:{search/open_page/find} Yes; include.
code_interpreter_call Built-in Python tool call id, status, container_id, code?, outputs? Yes; include.
local_shell_call Built-in local shell call id, call_id, action:{exec}, status Yes; include.
local_shell_call_output Built-in local shell result (if you sent it) id, call_id, output, status? Send from your app as input; include later for context.
image_generation_call Built-in image generation id, status, result? (base64) Yes; include.

Notes:

  • OutputMessage includes annotations you should keep (citations, file_path).
  • The presence of function_call_output/custom_tool_call_output/local_shell_call_output in output depends on whether you previously submitted them as input during an earlier turn or within a multi-step flow.

Table 3. What the API accepts as input (request.input)

The input field accepts either a string (equivalent to a user message) or a list of input items. The list allows original user content and replayed prior assistant/tool items.

input variant subtype/type purpose minimal shape summary
string n/a Simple user text “Hello, world” (equivalent to user role message).
Easy message (string) message (role=user/assistant/system/developer) Add a role-annotated message. {type:“message”, role, content:string} only (not typed)
Input text content input_text Text content part {type:“input_text”, text}.
Input image content input_image Vision input {type:“input_image”, image_url or file_id, detail:“low”
Input file content input_file File/PDF input {type:“input_file”, file_id? filename? file_url? file_data?}.
Input audio input_audio Audio input to model {type:“input_audio”, input_audio:{data: base64, format:“mp3”
Assistant message (replay) message (role=assistant) or OutputMessage Preserve the assistant’s last reply Best: include the OutputMessage object you received, as-is (id, role, content). Alternatively send Easy message with role=assistant.
Reasoning (encrypted) reasoning Preserve model chain-of-thought (encrypted) {type:“reasoning”, id, encrypted_content}. Only if you included it in previous output. Do not pass summary.
Tool call (function) function_call Replay what model did Copy the prior item: {type:“function_call”, call_id, name, arguments}.
Tool output (function) function_call_output Return your function result {type:“function_call_output”, call_id, output:string or content}.
Tool call (custom) custom_tool_call Replay what model did Copy prior item: {type:“custom_tool_call”, call_id, name, input}.
Tool output (custom) custom_tool_call_output Return your custom tool result {type:“custom_tool_call_output”, call_id, output:string or content}.
MCP tool call mcp_call Replay MCP call Copy prior item: {type:“mcp_call”, id, server_label, name, arguments, output?, error?, status}.
MCP list tools mcp_list_tools Replay MCP discovery Copy prior item: {type:“mcp_list_tools”, id, server_label, tools, error?}.
MCP approval request mcp_approval_request Replay request Copy prior item: {type:“mcp_approval_request”, id, server_label, name, arguments}.
MCP approval response mcp_approval_response Supply approval decision {type:“mcp_approval_response”, approval_request_id, approve, reason?}.
File search call file_search_call Replay call/results Copy prior item: {type:“file_search_call”, id, status, queries, results?}.
Web search call web_search_call Replay call Copy prior item: {type:“web_search_call”, id, status, action{…}}.
Code interpreter call code_interpreter_call Replay call/results Copy prior item: {type:“code_interpreter_call”, id, status, container_id, code?, outputs?}.
Local shell call local_shell_call Replay call Copy prior item: {type:“local_shell_call”, id, call_id, action{exec}, status}.
Local shell result local_shell_call_output Return your shell output {type:“local_shell_call_output”, id?, call_id, output, status?}.
Image generation call image_generation_call Replay call/result Copy prior item: {type:“image_generation_call”, id, status, result?}.
Item reference item_reference Refer to an earlier item by id {type:“item_reference”, id}. Useful if you persist items and want to reference rather than inline.

Tips:

  • For replaying assistant replies, you can embed the exact OutputMessage objects you received (no transformation).
  • For user messages with images/files/audio, use input_* content parts inside a message item with role=user.

Table 4. Output → Input mapping (stateless conversation replay)

This is the “do-this-in-your-code” table. Every row shows how an output item from the last turn becomes one or more input items for the next turn.

last-turn output item (from response.completed.output) next-turn input item to add transform needed? notes
message (assistant) OutputMessage (same object) No Include id, role=assistant, content, annotations intact. This preserves the assistant response verbatim.
message (assistant) with refusal OutputMessage (same object) No Keep refusal content part as-is to preserve context of refusal.
reasoning (with encrypted_content) reasoning Yes (drop summary) Only include if encrypted_content is present (you requested include: [“reasoning.encrypted_content”]). Set {type:“reasoning”, id, encrypted_content}. Do not include summary or plaintext content.
reasoning (no encrypted_content) — — Do not include. Reasoning summary/text is not useful input.
function_call (model issued) function_call No Copy {type, call_id, name, arguments}. Also prepare to send function_call_output with your tool’s result.
— (your tool ran) function_call_output You create this {type:“function_call_output”, call_id (from function_call), output:string or content}.
custom_tool_call (model issued) custom_tool_call No Copy {type, call_id, name, input}. Also prepare custom_tool_call_output.
— (your custom tool ran) custom_tool_call_output You create this {type:“custom_tool_call_output”, call_id, output}.
mcp_call mcp_call No Copy the entire item including arguments, output?, error?, status; keeps the MCP interaction in context.
mcp_list_tools mcp_list_tools No Copy the item; helps the model remember tool inventory.
mcp_approval_request mcp_approval_request No Copy the item if you need the approval in context.
— (you approved/rejected) mcp_approval_response You create this {type:“mcp_approval_response”, approval_request_id, approve, reason?}.
file_search_call file_search_call No Copy; results help with continuity and citations.
web_search_call web_search_call No Copy; action details maintain trace of browsing.
code_interpreter_call code_interpreter_call No Copy; includes container_id, code, outputs.
local_shell_call local_shell_call No Copy; action.exec, status preserved.
— (your shell executed) local_shell_call_output You create this {type:“local_shell_call_output”, call_id, output}.
image_generation_call image_generation_call No Copy; include result if present for downstream reference.

Important:

  • Order matters. Build input by concatenating, in order, all items from previous turns (user → assistant → tool calls → tool outputs → …), then append the new user request for this turn.
  • If your org is stateless/zero-retention, always replay everything you want the model to “remember”, including tool calls and their outputs.

Table 5. Reasoning tokens: what to return vs. what to ignore

you saw in output what to include next turn why
reasoning item with encrypted_content (you set include:[“reasoning.encrypted_content”]) Include {type:“reasoning”, id, encrypted_content}. This preserves the chain-of-thought for the model under encryption; only OpenAI can decrypt it.
reasoning.summary (summary_text parts) Do NOT include in input. Summaries are for UI/debug only; they are not used by the model.
reasoning.content (reasoning_text) Do NOT include in input. Plaintext reasoning is not accepted/needed; prefer encrypted form.
No reasoning item present Nothing to include. Not all models/requests return reasoning items.

Table 6. Allowed original user inputs (to add alongside replayed context)

role content parts example uses
user input_text General instructions/questions.
user input_image (image_url or file_id; detail) Vision tasks (describe image, OCR, etc.).
user input_file (file_id/filename/file_url/file_data) PDF/doc analysis, RAG ingestion.
user input_audio (data, format=mp3 wav)
developer/system input_text/image/file/audio Policy or tool guidance higher in instruction hierarchy.
assistant Generally you replay the OutputMessage you got Keep exact assistant message for continuity.

Recommended request-building order (stateless replay)

  • If you are not using conversation storage:

    1. Start input as an empty list.
    2. Append the prior turn’s items in the exact order they appeared logically (user message, assistant message, tool calls, tool outputs, MCP calls, etc.).
    3. If you captured reasoning.encrypted_content, append a reasoning item with encrypted_content (do not add summary).
    4. Append the new user message for this turn (message with role=user and appropriate content parts).
    5. Send the request with stream:true if you want streaming.
  • If you are using conversation:{id} and store:true:

    • You typically only send the new user message; prior items are auto-added by OpenAI. Avoid mixing conversation with manual previous_response_id.

Practical notes

  • response.completed has the authoritative final output list; do your output→input mapping from there rather than by stitching deltas yourself.
  • For function/custom/local shell tools: the output objects with type *_output are produced by you and must be sent back as input, keyed by call_id.
  • Only response.completed contains usage; if you must meter, read it there.
  • Audio is unannounced, only breadcrumbs are there. Chat Completions currently has a short-life audio ID server-side that must be re-sent (you are not allowed to simulate the AI producing audio). Responses likely will also deny you self-management.
  • Avoid passing reasoning summaries back; they are not used by the model.

Minimal example strategy (conceptual)

  • After collecting response.completed:
    • For each element in response.output:
      • If type in {message, function_call, custom_tool_call, mcp_call, mcp_list_tools, file_search_call, web_search_call, code_interpreter_call, local_shell_call, image_generation_call} → copy item as-is into next input.
      • If type=reasoning and encrypted_content is present → add {type:“reasoning”, id, encrypted_content}. Ignore summary/content.
    • Then append your next user message.

This approach ensures your next turn’s input mirrors the previous turn’s output in both type and order, preserving conversational coherence even without server-side conversation storage.

This is incomplete documentation for just one of the fields of “Responses” - not even getting into 53 SSE streaming events for you to create parsers for in your automatic iterator for functions, including error event that would have multiple statuses to take action on.

Thanks for the quick reply!

totally agreed the type of assistant role in items should be output_text - it’s just copied from the previous response call’s output field. However, the issue I mentioned is in input_message, it’s really difficult to understand the input_message couldn’t support input_textas a type for assistant role.

If the assistant role only support output_text, I feel it might be better to remove the assistant value in the document, because it’s causing confusion - I have the assistant as an option but I’m never able to use it

In addition, I doubt it used to be accepting the input_text as type for assistant role. I’m looking at XAI’s response api as well, which should following the openai spec to achieve compatibility. They only accept input_text as type for assistant role

https://docs.x.ai/docs/api-reference#create-new-response

You are correct - for the blocking of objects. The Responses API always had this behavior. If you want no-nonsense, you have to use Chat Completions.

You can read the “role” enums in the OpenAI OpenAPI specification for the “input” array of type “message” if an “object” if you really want to get to the nitty-gritty of why you are blocked from using assistant as a role for more than a string…

description: |
An item representing part of the context for the response to be
generated by the model. Can contain text, images, and audio inputs,
as well as previous assistant responses and tool call outputs.
discriminator:
propertyName: type
anyOf:
- x-resolved-from: '#/components/schemas/InputMessage'
  type: object
  title: Input message
  description: |
    A message input to the model with a role indicating instruction following
    hierarchy. Instructions given with the `developer` or `system` role take
    precedence over instructions given with the `user` role.
  properties:
    type:
      type: string
      description: |
        The type of the message input. Always set to `message`.
      enum:
        - message
      x-stainless-const: true
    role:
      type: string
      description: |
        The role of the message input. One of `user`, `system`, or `developer`.
      enum:
        - user
        - system
        - developer
    status:
      type: string
      description: |
        The status of item. One of `in_progress`, `completed`, or
        `incomplete`. Populated when items are returned via API.
      enum:
        - in_progress
        - completed
        - incomplete

Probably an easy validation shortcut to block the assistant from “pretending” to make images or other multimodal content itself, while giving you more simple string inputs for “content” as multi-shot.

The same is enforced on “instructions”, which also can be a complex array of messages and even example function calls.