Confirm this is an issue with the Python library and not an underlying OpenAI API
Describe the bug
Conversation histories don't always come back from the API. Assistant turns can be replayed
from storage, edited before being sent again, produced by other code, or carried over from
somewhere else. A turn like that has no OpenAI message id, because no API call ever produced
one. I'd like to pass it in input and have the request types support that.
The only version the types accept makes me fill in an id and a status I don't have:
conversation: ResponseInputParam = [
{"role": "user", "content": [{"type": "input_text", "text": "2+3=?"}]},
{
"type": "message",
"id": "msg_fake_msg_id", # <-- made up
"status": "completed", # <-- made up
"role": "assistant",
"content": [
{"type": "output_text", "text": "5", "annotations": []},
{"type": "output_text", "text": "something else?", "annotations": []},
],
},
{"role": "user", "content": [{"type": "input_text", "text": "what's the square of the result?"}]},
]
The id is the part that worries me, since ids plausibly take part in prompt caching. To be
safe about it I'd have to invent a scheme that derives the same synthetic id for the same
conversation prefix every time and keep that stable across versions. That's real work whose
only purpose is keeping the type checker quiet.
There are two other ways to write the same turn. Neither needs an id, and they fail in
opposite directions.
The first one just leaves id and status out, and the API is fine with it: the request goes
through and comes back with a normal answer (checked against the live endpoint, openai 2.48.0).
Only the types object, which suggests those two fields are the SDK's requirement rather than
the service's:
{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "5", "annotations": []}, # type: ignore
{"type": "output_text", "text": "something else?", "annotations": []}, # type: ignore
],
},
Writing # type: ignore (or cast()) to send a payload the API accepts happily defeats the
point of having typed requests in the first place.
The second one type-checks with nothing suppressed, then fails on the server:
{
"role": "assistant",
"content": [
{"type": "input_text", "text": "5"},
{"type": "input_text", "text": "something else?"},
],
},
{"error": {"message": "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.",
"type": "invalid_request_error", "param": "input[1].content[0]", "code": "invalid_value"}}
So out of the three, the one that both type-checks and works is the one with made-up values in
it.
Expected behavior
Some way to express a client-authored assistant turn that type-checks and doesn't require
inventing field values.
Related issues
The id requirement also came up in #2501, and #3015 proposes relaxing it, though status is
required as well so the workarounds above would still be needed. #2323 looks adjacent.
To Reproduce
Pass each of the three snippets above to client.responses.create(model=..., input=conversation)
with a type checker enabled. In order: the first needs the made-up id and status, the second
needs # type: ignore, the third type-checks and then returns the 400 above.
Code snippets
.env:
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5
app.py:
import os
from dotenv import load_dotenv
from openai import OpenAI
from openai.types.responses import ResponseInputParam
load_dotenv()
client = OpenAI()
def create_conversation(option: int) -> ResponseInputParam:
if option == 1:
return [
{
"role": "user",
"content": [{"type": "input_text", "text": "2+3=?"}],
},
{
"type": "message",
"id": "msg_fake_msg_id",
"status": "completed",
"role": "assistant",
"content": [
{"type": "output_text", "text": "5", "annotations": []},
{
"type": "output_text",
"text": "something else?",
"annotations": [],
},
],
},
{
"role": "user",
"content": [
{"type": "input_text", "text": "what's the square of the result?"}
],
},
]
if option == 2:
return [
{
"role": "user",
"content": [{"type": "input_text", "text": "2+3=?"}],
},
{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "5", "annotations": []}, # type: ignore
{"type": "output_text", "text": "something else?", "annotations": []}, # type: ignore
],
},
{
"role": "user",
"content": [
{"type": "input_text", "text": "what's the square of the result?"}
],
},
]
return [
{
"role": "user",
"content": [{"type": "input_text", "text": "2+3=?"}],
},
{
"role": "assistant",
"content": [
{"type": "input_text", "text": "5"},
{"type": "input_text", "text": "something else?"},
],
},
{
"role": "user",
"content": [
{"type": "input_text", "text": "what's the square of the result?"}
],
},
]
for i in [1, 2, 3]:
response = client.responses.create(
model=os.environ["OPENAI_MODEL"],
input=create_conversation(i),
)
print(response.output_text)
OS
macOS 14.6
Python version
3.11.13
Library version
openai 2.48.0 (checked with pyright 1.1.411)
Confirm this is an issue with the Python library and not an underlying OpenAI API
Describe the bug
Conversation histories don't always come back from the API. Assistant turns can be replayed
from storage, edited before being sent again, produced by other code, or carried over from
somewhere else. A turn like that has no OpenAI message id, because no API call ever produced
one. I'd like to pass it in
inputand have the request types support that.The only version the types accept makes me fill in an id and a status I don't have:
The
idis the part that worries me, since ids plausibly take part in prompt caching. To besafe about it I'd have to invent a scheme that derives the same synthetic id for the same
conversation prefix every time and keep that stable across versions. That's real work whose
only purpose is keeping the type checker quiet.
There are two other ways to write the same turn. Neither needs an id, and they fail in
opposite directions.
The first one just leaves
idandstatusout, and the API is fine with it: the request goesthrough and comes back with a normal answer (checked against the live endpoint, openai 2.48.0).
Only the types object, which suggests those two fields are the SDK's requirement rather than
the service's:
{ "type": "message", "role": "assistant", "content": [ {"type": "output_text", "text": "5", "annotations": []}, # type: ignore {"type": "output_text", "text": "something else?", "annotations": []}, # type: ignore ], },Writing
# type: ignore(orcast()) to send a payload the API accepts happily defeats thepoint of having typed requests in the first place.
The second one type-checks with nothing suppressed, then fails on the server:
{ "role": "assistant", "content": [ {"type": "input_text", "text": "5"}, {"type": "input_text", "text": "something else?"}, ], },{"error": {"message": "Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'.", "type": "invalid_request_error", "param": "input[1].content[0]", "code": "invalid_value"}}So out of the three, the one that both type-checks and works is the one with made-up values in
it.
Expected behavior
Some way to express a client-authored assistant turn that type-checks and doesn't require
inventing field values.
Related issues
The
idrequirement also came up in #2501, and #3015 proposes relaxing it, thoughstatusisrequired as well so the workarounds above would still be needed. #2323 looks adjacent.
To Reproduce
Pass each of the three snippets above to
client.responses.create(model=..., input=conversation)with a type checker enabled. In order: the first needs the made-up
idandstatus, the secondneeds
# type: ignore, the third type-checks and then returns the 400 above.Code snippets
.env:
app.py:
OS
macOS 14.6
Python version
3.11.13
Library version
openai 2.48.0 (checked with pyright 1.1.411)