`responses` corrupts previous conversations `assistant` item

Hello. I’m not sure if I’m being daft or there is some obscure rule that I’m not aware of, or I’ve stumbled upon a bug, but here goes.

The context is as follows:

I am running an AI chat bot.
Some of the messages that come from the chat bot are hard coded.
I am using conversations.

The order of events is as follows.

I store a hard coded message that I sent to the user as part of the conversation so that the AI has context and is aware of the message that I sent. I use the conversations/id/items POST endpoint to store the message. I get an item_id back and the message can be seen as part of the conversation if I make a call to the conversations/id/items GET endpoint. So far so good.

If I subsequently make a call to the responses POST endpoint referencing the conversation ID, the AI response will be added to the conversation, but will corrupt the hard coded message that I just stored (and thus remove it from the conversation). If I now query the conversationds/id/items GET endpoint with the item_id of the message I stored, I get the following:

{
    "id": "msg_69f4aa450a5481969b74c76cb0147c800197e6a5b67615bf",
    "type": "message",
    "status": "completed",
    "content": [
        {
            "type": "text",
            "text": "<unknown message>"
        }
    ],
    "role": "unknown"
}

What am I missing?

I can’t repro that.

Let’s make a script:

#!/usr/bin/env bash
set -euo pipefail

: "${OPENAI_API_KEY:?Set OPENAI_API_KEY first}"

BASE="https://api.openai.com/v1"
AUTH=(-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json")

echo "1) Create conversation"
CONV_ID=$(
  curl -sS "$BASE/conversations" \
    "${AUTH[@]}" \
    -d '{}' | jq -r '.id'
)
echo "CONV_ID=$CONV_ID"

echo "2) Add hard-coded bot/assistant message to conversation"
ADD_ITEM_JSON=$(
  curl -sS "$BASE/conversations/$CONV_ID/items" \
    "${AUTH[@]}" \
    -d '{
      "items": [
        {
          "type": "message",
          "role": "assistant",
          "content": "This is a hard-coded bot message that should remain in the conversation."
        }
      ]
    }'
)
echo "$ADD_ITEM_JSON" | jq .

ITEM_ID=$(echo "$ADD_ITEM_JSON" | jq -r '.data[0].id')
echo "ITEM_ID=$ITEM_ID"

echo "3) Retrieve that item before Responses call"
curl -sS "$BASE/conversations/$CONV_ID/items/$ITEM_ID" \
  "${AUTH[@]}" | jq .

echo "4) List all items before Responses call"
curl -sS "$BASE/conversations/$CONV_ID/items?order=asc" \
  "${AUTH[@]}" | jq .

echo "5) Create a model response using the conversation"
curl -sS "$BASE/responses" \
  "${AUTH[@]}" \
  -d "{
    \"model\": \"gpt-4.1-mini\",
    \"conversation\": \"$CONV_ID\",
    \"input\": \"Please respond briefly. What was the hard-coded bot message?\"
  }" | jq .

echo "6) Retrieve original hard-coded item after Responses call"
curl -sS "$BASE/conversations/$CONV_ID/items/$ITEM_ID" \
  "${AUTH[@]}" | jq .

echo "7) List all items after Responses call"
curl -sS "$BASE/conversations/$CONV_ID/items?order=asc" \
  "${AUTH[@]}" | jq .

now run it:

export OPENAI_API_KEY="sk-..."
chmod +x repro.sh
./repro.sh

result:

1) Create conversation
CONV_ID=conv_<redacted>

2) Add hard-coded bot/assistant message to conversation
{
  "object": "list",
  "data": [
    {
      "id": "msg_<redacted>",
      "type": "message",
      "status": "completed",
      "content": [
        {
          "type": "input_text",
          "text": "This is a hard-coded bot message that should remain in the conversation."
        }
      ],
      "role": "assistant"
    }
  ],
  "first_id": "msg_<redacted>",
  "has_more": false,
  "last_id": "msg_<redacted>"
}
ITEM_ID=msg_<redacted>

3) Retrieve that item before Responses call
{
  "id": "msg_<redacted>",
  "type": "message",
  "status": "completed",
  "content": [
    {
      "type": "input_text",
      "text": "This is a hard-coded bot message that should remain in the conversation."
    }
  ],
  "role": "assistant"
}

4) List all items before Responses call
{
  "object": "list",
  "data": [
    {
      "id": "msg_<redacted>",
      "type": "message",
      "status": "completed",
      "content": [
        {
          "type": "input_text",
          "text": "This is a hard-coded bot message that should remain in the conversation."
        }
      ],
      "role": "assistant"
    }
  ],
  "first_id": "msg_<redacted>",
  "has_more": false,
  "last_id": "msg_<redacted>"
}

5) Create a model response using the conversation
{
  "id": "resp_<redacted>",
  "object": "response",
  "created_at": 1777658589,
  "status": "completed",
  "background": false,
  "billing": {
    "payer": "openai"
  },
  "completed_at": 1777658591,
  "conversation": {
    "id": "conv_<redacted>"
  },
  "error": null,
  "frequency_penalty": 0.0,
  "incomplete_details": null,
  "instructions": null,
  "max_output_tokens": null,
  "max_tool_calls": null,
  "model": "gpt-4.1-mini-2025-04-14",
  "moderation": null,
  "output": [
    {
      "id": "msg_<redacted>",
      "type": "message",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "annotations": [],
          "logprobs": [],
          "text": "The hard-coded bot message was:  \n\"This is a hard-coded bot message that should remain in the conversation.\""
        }
      ],
      "role": "assistant"
    }
  ],
  "parallel_tool_calls": true,
  "presence_penalty": 0.0,
  "previous_response_id": null,
  "prompt_cache_key": null,
  "prompt_cache_retention": "in_memory",
  "reasoning": {
    "effort": null,
    "summary": null
  },
  "safety_identifier": null,
  "service_tier": "default",
  "store": true,
  "temperature": 1.0,
  "text": {
    "format": {
      "type": "text"
    },
    "verbosity": "medium"
  },
  "tool_choice": "auto",
  "tools": [],
  "top_logprobs": 0,
  "top_p": 1.0,
  "truncation": "disabled",
  "usage": {
    "input_tokens": 37,
    "input_tokens_details": {
      "cached_tokens": 0
    },
    "output_tokens": 23,
    "output_tokens_details": {
      "reasoning_tokens": 0
    },
    "total_tokens": 60
  },
  "user": null,
  "metadata": {}
}

6) Retrieve original hard-coded item after Responses call
{
  "id": "msg_<redacted>",
  "type": "message",
  "status": "completed",
  "content": [
    {
      "type": "input_text",
      "text": "This is a hard-coded bot message that should remain in the conversation."
    }
  ],
  "role": "assistant"
}

7) List all items after Responses call
{
  "object": "list",
  "data": [
    {
      "id": "msg_<redacted>",
      "type": "message",
      "status": "completed",
      "content": [
        {
          "type": "input_text",
          "text": "This is a hard-coded bot message that should remain in the conversation."
        }
      ],
      "role": "assistant"
    },
    {
      "id": "msg_<redacted>",
      "type": "message",
      "status": "completed",
      "content": [
        {
          "type": "input_text",
          "text": "Please respond briefly. What was the hard-coded bot message?"
        }
      ],
      "role": "user"
    },
    {
      "id": "msg_<redacted>",
      "type": "message",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "annotations": [],
          "logprobs": [],
          "text": "The hard-coded bot message was:  \n\"This is a hard-coded bot message that should remain in the conversation.\""
        }
      ],
      "role": "assistant"
    }
  ],
  "first_id": "msg_<redacted>",
  "has_more": false,
  "last_id": "msg_<redacted>"
}

I wonder if your data shape is wrong or you are using the wrong properties?

Reproduction tip to frustrate endpoints with tech debt and bloat:

Put actual verbatim Responses outputs into conversations as items, with agreeing schemas where they are also echo-able as inputs, and hosted tool call multi-turn tasks with reasoning.

You will note “phase” as a field in Assistant responses when using GPT-5.4 or greater in multi-turn tasks.

Thank you for your time on the matter, I was hoping it was something obvious, but I’m still scratching my head. Here’s a bash script to replicate my issue:

#!/usr/bin/env bash
set -eu

: "${OPENAI_API_KEY:?Set OPENAI_API_KEY first}"

BASE="https://api.openai.com/v1"
MODEL="${MODEL:-gpt-5.4-mini}"
SLEEP_AFTER_ASSISTANT_ADD="${SLEEP_AFTER_ASSISTANT_ADD:-2}"

AUTH=(
  -H "Authorization: Bearer $OPENAI_API_KEY"
  -H "Content-Type: application/json"
)

SYSTEM_TEXT="System message containing instructions."
USER_TEXT="Initial user message"
ASSISTANT_TEXT="Assistant message that will disappear."
RESPONSES_INPUT_TEXT="Has the user provided answers to the outstanding fields? "

list_items() {
  curl -sS --globoff \
    "$BASE/conversations/$CONV_ID/items?order=asc&include[]=message.input_image.image_url&limit=100" \
    "${AUTH[@]}"
}

retrieve_item() {
  local item_id="$1"
  curl -sS --globoff \
    "$BASE/conversations/$CONV_ID/items/$item_id?include[]=message.input_image.image_url" \
    "${AUTH[@]}"
}

echo
echo "1) Create conversation with initial system message"

CREATE_CONV_JSON=$(
  jq -n --arg text "$SYSTEM_TEXT" '{
    items: [
      {
        role: "system",
        content: $text,
        type: "message"
      }
    ]
  }'
)

CREATE_CONV_RESPONSE=$(
  curl -sS "$BASE/conversations" \
    "${AUTH[@]}" \
    -d "$CREATE_CONV_JSON"
)

echo "$CREATE_CONV_RESPONSE" | jq .

CONV_ID=$(echo "$CREATE_CONV_RESPONSE" | jq -r '.id')

if [[ "$CONV_ID" == "null" || -z "$CONV_ID" ]]; then
  echo "ERROR: Failed to create conversation"
  exit 1
fi

echo "CONV_ID=$CONV_ID"


echo
echo "2) Add user message to conversation"

ADD_USER_JSON=$(
  jq -n --arg text "$USER_TEXT" '{
    items: [
      {
        type: "message",
        role: "user",
        content: [
          {
            type: "input_text",
            text: $text
          }
        ]
      }
    ]
  }'
)

ADD_USER_RESPONSE=$(
  curl -sS "$BASE/conversations/$CONV_ID/items" \
    "${AUTH[@]}" \
    -d "$ADD_USER_JSON"
)

echo "$ADD_USER_RESPONSE" | jq .

USER_ITEM_ID=$(echo "$ADD_USER_RESPONSE" | jq -r '.data[0].id')
echo "USER_ITEM_ID=$USER_ITEM_ID"


echo
echo "3) Add hard-coded assistant EasyInputMessage to conversation"

ADD_ASSISTANT_JSON=$(
  jq -n --arg text "$ASSISTANT_TEXT" '{
    items: [
      {
        type: "message",
        role: "assistant",
        content: $text,
        phase: "final_answer"
      }
    ]
  }'
)

ADD_ASSISTANT_RESPONSE=$(
  curl -sS "$BASE/conversations/$CONV_ID/items" \
    "${AUTH[@]}" \
    -d "$ADD_ASSISTANT_JSON"
)

echo "$ADD_ASSISTANT_RESPONSE" | jq .

ASSISTANT_ITEM_ID=$(echo "$ADD_ASSISTANT_RESPONSE" | jq -r '.data[0].id')

if [[ "$ASSISTANT_ITEM_ID" == "null" || -z "$ASSISTANT_ITEM_ID" ]]; then
  echo "ERROR: Failed to create assistant item"
  exit 1
fi

echo "ASSISTANT_ITEM_ID=$ASSISTANT_ITEM_ID"


echo
echo "4) Retrieve hard-coded assistant item before Responses call"

RETRIEVE_BEFORE=$(retrieve_item "$ASSISTANT_ITEM_ID")
echo "$RETRIEVE_BEFORE" | jq .


echo
echo "5) List all items before Responses call"

LIST_BEFORE=$(list_items)
echo "$LIST_BEFORE" | jq .

echo
echo "5a) Check hard-coded assistant item is present before Responses call"

if echo "$LIST_BEFORE" | jq -e --arg id "$ASSISTANT_ITEM_ID" '.data[] | select(.id == $id)' >/dev/null; then
  echo "OK: Assistant item is present in list before Responses call"
else
  echo "ERROR: Assistant item is already missing before Responses call"
  exit 1
fi


echo
echo "6) Wait briefly to rule out immediate write/read timing issues"
echo "Sleeping ${SLEEP_AFTER_ASSISTANT_ADD}s..."
sleep "$SLEEP_AFTER_ASSISTANT_ADD"


echo
echo "7) Create model response using SAME conversation ID"

CREATE_RESPONSE_JSON=$(
  jq -n \
    --arg model "$MODEL" \
    --arg conv "$CONV_ID" \
    --arg text "$RESPONSES_INPUT_TEXT" \
    '{
      model: $model,
      conversation: $conv,
      store: true,
      input: [
        {
          content: $text,
          role: "system",
          type: "message"
        }
      ]
    }'
)

CREATE_RESPONSE_RESPONSE=$(
  curl -sS "$BASE/responses" \
    "${AUTH[@]}" \
    -d "$CREATE_RESPONSE_JSON"
)

echo "$CREATE_RESPONSE_RESPONSE" | jq .

RESPONSE_ID=$(echo "$CREATE_RESPONSE_RESPONSE" | jq -r '.id')
echo "RESPONSE_ID=$RESPONSE_ID"


echo
echo "8) Retrieve original hard-coded assistant item after Responses call"

RETRIEVE_AFTER=$(retrieve_item "$ASSISTANT_ITEM_ID")
echo "$RETRIEVE_AFTER" | jq .


echo
echo "9) List all items after Responses call"

LIST_AFTER=$(list_items)
echo "$LIST_AFTER" | jq .


echo
echo "10) Repro checks"

echo
echo "Check A: Is original hard-coded assistant item still present in the list?"

if echo "$LIST_AFTER" | jq -e --arg id "$ASSISTANT_ITEM_ID" '.data[] | select(.id == $id)' >/dev/null; then
  echo "NOT REPRODUCED: Original assistant item is still present in the conversation list"
else
  echo "REPRODUCED: Original assistant item is missing from the conversation list after Responses call"
fi


echo
echo "Check B: Does direct retrieval now return role=unknown and <unknown message>?"

if echo "$RETRIEVE_AFTER" | jq -e '
  .role == "unknown"
  and (.content | length > 0)
  and (.content[0].text == "<unknown message>")
' >/dev/null; then
  echo "REPRODUCED: Direct retrieval returns role=unknown and <unknown message>"
else
  echo "NOT REPRODUCED: Direct retrieval did not return the unknown-message placeholder"
fi


echo
echo "Check C: Show before/after summary for the original assistant item"

echo "Before:"
echo "$RETRIEVE_BEFORE" | jq '{
  id,
  role,
  type,
  status,
  content
}'

echo "After:"
echo "$RETRIEVE_AFTER" | jq '{
  id,
  role,
  type,
  status,
  content
}'


echo
echo "Done."
echo "CONV_ID=$CONV_ID"
echo "ASSISTANT_ITEM_ID=$ASSISTANT_ITEM_ID"
echo "MODEL=$MODEL"

Here’s a play by play of what I’m doing. Hopefully this will make apparent what amateur hour mistake I’ve made. Or we’ve stumbled upon a little bug that I’m going to have to navigate around until our AI overlords fix it.

I create a conversation:

curl --location 'https://api.openai.com/v1/conversations' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer TOKEN' \
--data '{
    "items": [
        {
            "role": "system",
            "content": "System message containing instructions.",
            "type": "message"
        }
    ]
}'

I get the expected response:

{
    "id": "conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622",
    "object": "conversation",
    "created_at": 1777698587,
    "metadata": {}
}

I add a user message to the conversation:

curl --location 'https://api.openai.com/v1/conversations/conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622/items' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer TOKEN' \
--data '{
    "items": [
        {
            "type": "message",
            "role": "user",
            "content": [
                {
                    "type": "input_text",
                    "text": "Initial user message"
                }
            ]
        }
    ]
}'

I get the expected response:

{
    "object": "list",
    "data": [
        {
            "id": "msg_69f5878674a481939362d4e7bed94b3808829a4c83cbc622",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "Initial user message"
                }
            ],
            "role": "user"
        }
    ],
    "first_id": "msg_69f5878674a481939362d4e7bed94b3808829a4c83cbc622",
    "has_more": false,
    "last_id": "msg_69f5878674a481939362d4e7bed94b3808829a4c83cbc622"
}

I then add a hard coded assistant message. This message will eventually become corrupted and disappear from the list:

curl --location 'https://api.openai.com/v1/conversations/conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622/items' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer TOKEN' \
--data '{
    "items": [
        {
            "type": "message",
            "role": "assistant",
            "content": "Assistant message that will disappear.",
            "phase": "final_answer"
        }
    ]
}'

I get the expected response:

{
    "object": "list",
    "data": [
        {
            "id": "msg_69f587a159c881939bd111a2c71142c908829a4c83cbc622",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "Assistant message that will disappear."
                }
            ],
            "role": "assistant"
        }
    ],
    "first_id": "msg_69f587a159c881939bd111a2c71142c908829a4c83cbc622",
    "has_more": false,
    "last_id": "msg_69f587a159c881939bd111a2c71142c908829a4c83cbc622"
}

To double check that the message was added to the conversation, I make a call to get all items in the conversation so far:

curl --location --globoff 'https://api.openai.com/v1/conversations/conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622/items?order=asc&include[]=message.input_image.image_url&limit=100' \
--header 'Authorization: Bearer TOKEN'

The hard coded assistant message is there:

{
    "object": "list",
    "data": [
        {
            "id": "msg_69f5871bb0bc8193be30f0b1e80313a208829a4c83cbc622",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "System message containing instructions."
                }
            ],
            "role": "system"
        },
        {
            "id": "msg_69f5878674a481939362d4e7bed94b3808829a4c83cbc622",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "Initial user message"
                }
            ],
            "role": "user"
        },
        {
            "id": "msg_69f587a159c881939bd111a2c71142c908829a4c83cbc622",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "Assistant message that will disappear."
                }
            ],
            "role": "assistant"
        }
    ],
    "first_id": "msg_69f5871bb0bc8193be30f0b1e80313a208829a4c83cbc622",
    "has_more": false,
    "last_id": "msg_69f587a159c881939bd111a2c71142c908829a4c83cbc622"
}

I then make a call to the responses endpoint:

curl --location 'https://api.openai.com/v1/responses' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer TOKEN' \
--data '{
    "model": "gpt-5.4-mini",
    "conversation": "conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622",
    "input": [
        {
            "content": "Has the user provided answers to the outstanding fields? ",
            "role": "system",
            "type": "message"
        }
    ]
}'

And get a response from it:

{
    "id": "resp_08829a4c83cbc6220069f587f985c88193a5a04f15b4d5406e",
    "object": "response",
    "created_at": 1777698810,
    "status": "completed",
    "background": false,
    "billing": {
        "payer": "developer"
    },
    "completed_at": 1777698811,
    "conversation": {
        "id": "conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622"
    },
    "error": null,
    "frequency_penalty": 0.0,
    "incomplete_details": null,
    "instructions": null,
    "max_output_tokens": null,
    "max_tool_calls": null,
    "model": "gpt-5.4-mini-2026-03-17",
    "moderation": null,
    "output": [
        {
            "id": "msg_08829a4c83cbc6220069f587fafab48193bcb5c61162b541a5",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "output_text",
                    "annotations": [],
                    "logprobs": [],
                    "text": "No."
                }
            ],
            "phase": "final_answer",
            "role": "assistant"
        }
    ],
    "parallel_tool_calls": true,
    "presence_penalty": 0.0,
    "previous_response_id": null,
    "prompt_cache_key": null,
    "prompt_cache_retention": "in_memory",
    "reasoning": {
        "effort": "none",
        "summary": null
    },
    "safety_identifier": null,
    "service_tier": "default",
    "store": true,
    "temperature": 1.0,
    "text": {
        "format": {
            "type": "text"
        },
        "verbosity": "medium"
    },
    "tool_choice": "auto",
    "tools": [],
    "top_logprobs": 0,
    "top_p": 0.98,
    "truncation": "disabled",
    "usage": {
        "input_tokens": 45,
        "input_tokens_details": {
            "cached_tokens": 0
        },
        "output_tokens": 6,
        "output_tokens_details": {
            "reasoning_tokens": 0
        },
        "total_tokens": 51
    },
    "user": null,
    "metadata": {}
}

I then make another call to get all items in the conversation:

curl --location --globoff 'https://api.openai.com/v1/conversations/conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622/items?order=asc&include[]=message.input_image.image_url&limit=100' \
--header 'Authorization: Bearer TOKEN'

But now that hard coded assistant message has disappeared:

{
    "object": "list",
    "data": [
        {
            "id": "msg_69f5871bb0bc8193be30f0b1e80313a208829a4c83cbc622",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "System message containing instructions."
                }
            ],
            "role": "system"
        },
        {
            "id": "msg_69f5878674a481939362d4e7bed94b3808829a4c83cbc622",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "Initial user message"
                }
            ],
            "role": "user"
        },
        {
            "id": "msg_08829a4c83cbc6220069f587fa76f48193aee927a5bcde3104",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "input_text",
                    "text": "Has the user provided answers to the outstanding fields? "
                }
            ],
            "role": "system"
        },
        {
            "id": "msg_08829a4c83cbc6220069f587fafab48193bcb5c61162b541a5",
            "type": "message",
            "status": "completed",
            "content": [
                {
                    "type": "output_text",
                    "annotations": [],
                    "logprobs": [],
                    "text": "No."
                }
            ],
            "role": "assistant"
        }
    ],
    "first_id": "msg_69f5871bb0bc8193be30f0b1e80313a208829a4c83cbc622",
    "has_more": false,
    "last_id": "msg_08829a4c83cbc6220069f587fafab48193bcb5c61162b541a5"
}

If I ask for it specifically:

curl --location --globoff 'https://api.openai.com/v1/conversations/conv_69f5871bb0948193bd42bd2a8649e89f08829a4c83cbc622/items/msg_69f587a159c881939bd111a2c71142c908829a4c83cbc622?order=asc&include[]=message.input_image.image_url&limit=100' \
--header 'Authorization: Bearer TOKEN'

It returns the <unknown message> tag:

{
    "id": "msg_69f587a159c881939bd111a2c71142c908829a4c83cbc622",
    "type": "message",
    "status": "completed",
    "content": [
        {
            "type": "text",
            "text": "<unknown message>"
        }
    ],
    "role": "unknown"
}

OK one slightly odd thing you are doing is you are appending a system message using responses after the conversation is well under way. Can you avoid that?

1. system    → original system
2. user      → original user
3. assistant → your injected assistant
4. system    → NEW system (from Responses input)

I would expect (and I’m probably not the only one) only one system message and then a series of user and assistant messages.

So your pattern is at least non-standard and might not be supported.

The role=system messages are red herrings. I could switch them all to role=developer and it wouldn’t make a difference. It could omit them and it still wouldn’t make a difference. I also tried switching the hard coded assistant messages from EasyInputMessage to a ResponseOutputMessage, that didn’t make a difference either.

It seems like all hard coded assistant messages synthetically inserted into a Conversation using the conversations/conversation_id/items POST endpoint, prior to a responses call that produces an AI generated message will be wiped as soon as the response from that call is returned.

The only way to synthetically insert an assistant message into a Conversation is to create it by making a deterministic call to responses where the message is presented to the AI with a request to simply reply with the exact message. It feels a little hacky in my opinion, but it seems to be the only thing that works. Something like this:

{
  "model": "gpt-5.4-mini",
  "conversation": "conv_...",
  "temperature": 0,
  "input": [
    {
      "role": "developer",
      "content": "Reply EXACTLY with the following text, no deviation:\n\n[HARD CODED ASSISTANT MESSAGE]"
    },
    {
      "role": "user",
      "content": "Start"
    }
  ]
}

To wrap up, here’s an extended bash script that inserts three hard coded assistant messages with different phases, followed by a user message, and then a Responses call, and all the assistant messages get wiped.

#!/usr/bin/env bash
set -eu

: "${OPENAI_API_KEY:?Set OPENAI_API_KEY first}"

BASE="https://api.openai.com/v1"
MODEL="gpt-5.4-mini"

AUTH=(
  -H "Authorization: Bearer $OPENAI_API_KEY"
  -H "Content-Type: application/json"
)

SYSTEM_TEXT="System message containing instructions."
USER_TEXT="Initial user message"
SECOND_USER_TEXT="Second user message before Responses call"
DEVELOPER_TEXT="Has the user provided answers to the outstanding fields? "

list_items() {
  curl -sS --globoff \
    "$BASE/conversations/$CONV_ID/items?order=asc&limit=100" \
    "${AUTH[@]}"
}

retrieve_item() {
  ITEM_ID="$1"
  curl -sS --globoff \
    "$BASE/conversations/$CONV_ID/items/$ITEM_ID" \
    "${AUTH[@]}"
}

echo "1) Create conversation with system message"
CREATE_CONV_RESPONSE=$(
  curl -sS "$BASE/conversations" \
    "${AUTH[@]}" \
    -d "$(jq -n --arg text "$SYSTEM_TEXT" '{
      items: [
        {
          role: "system",
          content: $text,
          type: "message"
        }
      ]
    }')"
)

echo "$CREATE_CONV_RESPONSE" | jq .
CONV_ID=$(echo "$CREATE_CONV_RESPONSE" | jq -r '.id')
echo "CONV_ID=$CONV_ID"

echo
echo "2) Add first user message"
curl -sS "$BASE/conversations/$CONV_ID/items" \
  "${AUTH[@]}" \
  -d "$(jq -n --arg text "$USER_TEXT" '{
    items: [
      {
        type: "message",
        role: "user",
        content: [
          {
            type: "input_text",
            text: $text
          }
        ]
      }
    ]
  }')" | jq .

echo
echo "3) Add assistant messages with different phase variants"

ASSISTANT_IDS=()

for PHASE in "final_answer" "commentary" ""; do
  LABEL="${PHASE:-none}"
  TEXT="Assistant (${LABEL}) message"

  echo "Adding assistant message with phase: ${LABEL}"

  ADD_ASSISTANT_RESPONSE=$(
    curl -sS "$BASE/conversations/$CONV_ID/items" \
      "${AUTH[@]}" \
      -d "$(jq -n \
        --arg text "$TEXT" \
        --arg phase "$PHASE" \
        '{
          items: [
            (
              {
                type: "message",
                role: "assistant",
                content: [
                  {
                    type: "output_text",
                    text: $text
                  }
                ]
              }
              + (if $phase != "" then {phase: $phase} else {} end)
            )
          ]
        }')"
  )

  echo "$ADD_ASSISTANT_RESPONSE" | jq .

  ID=$(echo "$ADD_ASSISTANT_RESPONSE" | jq -r '.data[0].id')
  ASSISTANT_IDS+=("$ID")
done

echo
echo "4) Add SECOND user message"
curl -sS "$BASE/conversations/$CONV_ID/items" \
  "${AUTH[@]}" \
  -d "$(jq -n --arg text "$SECOND_USER_TEXT" '{
    items: [
      {
        type: "message",
        role: "user",
        content: [
          {
            type: "input_text",
            text: $text
          }
        ]
      }
    ]
  }')" | jq .

echo
echo "5) List all items BEFORE Responses call"
LIST_BEFORE=$(list_items)
echo "$LIST_BEFORE" | jq .

echo
echo "6) Verify assistant items exist BEFORE Responses"
for ID in "${ASSISTANT_IDS[@]}"; do
  if echo "$LIST_BEFORE" | jq -e --arg id "$ID" '.data[] | select(.id == $id)' >/dev/null; then
    echo "OK: $ID present"
  else
    echo "ERROR: $ID missing before Responses"
  fi
done

echo
echo "7) Create response using SAME conversation"
CREATE_RESPONSE_RESPONSE=$(
  curl -sS "$BASE/responses" \
    "${AUTH[@]}" \
    -d "$(jq -n \
      --arg model "$MODEL" \
      --arg conv "$CONV_ID" \
      --arg text "$DEVELOPER_TEXT" \
      '{
        model: $model,
        conversation: $conv,
        store: true,
        input: [
          {
            role: "developer",
            type: "message",
            content: $text
          }
        ]
      }')"
)

echo "$CREATE_RESPONSE_RESPONSE" | jq .

echo
echo "8) List all items AFTER Responses call"
LIST_AFTER=$(list_items)
echo "$LIST_AFTER" | jq .

echo
echo "9) Check assistant messages after Responses"

for ID in "${ASSISTANT_IDS[@]}"; do
  echo
  echo "Checking $ID"

  if echo "$LIST_AFTER" | jq -e --arg id "$ID" '.data[] | select(.id == $id)' >/dev/null; then
    echo "❌ Still present in list"
  else
    echo "✅ Missing from list (DISAPPEARED)"
  fi

  RETRIEVE=$(retrieve_item "$ID")
  echo "$RETRIEVE" | jq .

  if echo "$RETRIEVE" | jq -e '
    .role == "unknown"
    and (.content[0].text == "<unknown message>")
  ' >/dev/null; then
    echo "✅ Corrupted to <unknown message>"
  else
    echo "❌ Still valid"
  fi
done

echo
echo "Done."
echo "CONV_ID=$CONV_ID"
echo "ASSISTANT_IDS=${ASSISTANT_IDS[*]}" 

I am sure they have a very good reason for why they don’t allow synthetic assistant messages in a conversation, but it is a little frustrating either way. Thank you all for your help figuring this one out!

These are not the same thing, I believe. “developer” role is distinct: a developer message might be allowed mid stream.

However, you should be sticking to a single system message and not modify it or attempt to send another. Anything else should be an anti-pattern.

This feels like a bug to me, so I wouldn’t mark this as a solution and this should get some staff attention.

imho a solution is a confirmation this is fixed.

The developer and system roles are not the same, but with regards to whether that’s the cause of the problem, it’s not.

Even with all developer messages, or with no developer messages, as soon as I call the Responses end point after inserting synthetic assistant messages, the synthetic messages are corrupted/ignored/removed.

I take your point of leaving it unresolved and have marked it as such, I wasn’t aware staff review these threads. Even if they don’t classify it as a bug, they should at least update their documentation.

They very definitely do although as you can imagine with such a huge user base they are bandwidth constrained

Hey @all! appreciate you taking the time to flag this. We’re looking into it on our side and will post back when we have more to share.

- Sunny

Yes, this appears to be a bug and it is easily reproducible.

Looking for an unblocker, I think @merefield is on to something with the question about how instructions are being used here. To me, it looks like the hardcoded assistant message actually contains instructions. In that case, the hardcoded message could instead be added as an instruction to each model request.

Two other perspectives:

  • Replacing the corrupted hardcoded message after each call is probably the quickest fix, but it adds latency and is ultimately just a workaround. It also needs to happen after every Responses API call because the bug will reappear with each new assistant message.

  • If possible, use only the Responses API, since this issue does not appear there. The tradeoff is that you need to manage conversation state yourself instead of relying on server-side state management. Since you mentioned that you are still expanding your knowledge, this could also be a good opportunity to deepen your understanding of how chat applications are built.

Hope this helps!

Thank you for your suggestions.

What I have ended up doing is not sending assistant messages to be stored using the Conversations and instead sending them with prompts to just “reply with the message” via Responses. A bit hacky but doesn’t seem to slow things down too much.

I will still use the Conversations endpoint to store message. I don’t always need a response for each message in the conversations, and for user roles, it’s still a pretty good place to store context.

I would be happy to learn more about how others are building chat apps. So far, what I’ve built seem to work very well within our context, but I’m always keen to learn more. Got any suggestions on where I should look?

Good to know that you found a solution!
I also tried adding the hardcoded message as a user message, instead of using the assistant role. It works just as well.

Looking forward to see you around!